@zephyrdeng/pi-review 0.7.1 → 0.8.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 +37 -8
- package/README.zh-CN.md +31 -5
- package/dist/args.d.ts +4 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +61 -4
- package/dist/args.js.map +1 -1
- package/dist/args.test.js +62 -1
- package/dist/args.test.js.map +1 -1
- package/dist/child-process.d.ts +3 -0
- package/dist/child-process.d.ts.map +1 -1
- package/dist/child-process.js +27 -5
- package/dist/child-process.js.map +1 -1
- package/dist/child-process.test.js +42 -1
- package/dist/child-process.test.js.map +1 -1
- package/dist/cli-exit.test.js +14 -5
- package/dist/cli-exit.test.js.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/install-args.test.js +1 -1
- package/dist/install-args.test.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +2 -3
- package/dist/install.js.map +1 -1
- package/dist/json-events.d.ts +6 -0
- package/dist/json-events.d.ts.map +1 -1
- package/dist/json-events.js +48 -6
- package/dist/json-events.js.map +1 -1
- package/dist/json-events.test.js +20 -1
- package/dist/json-events.test.js.map +1 -1
- package/dist/loop.d.ts +28 -2
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +45 -3
- package/dist/loop.js.map +1 -1
- package/dist/loop.test.js +24 -1
- package/dist/loop.test.js.map +1 -1
- package/dist/matcher.test.js +1 -1
- package/dist/matcher.test.js.map +1 -1
- package/dist/meta-footer.d.ts +4 -8
- package/dist/meta-footer.d.ts.map +1 -1
- package/dist/meta-footer.js +19 -5
- package/dist/meta-footer.js.map +1 -1
- package/dist/meta-footer.test.js +11 -4
- package/dist/meta-footer.test.js.map +1 -1
- package/dist/panel-aggregate.test.js +1 -1
- package/dist/panel-aggregate.test.js.map +1 -1
- package/dist/panel-cancellation.test.js +33 -2
- package/dist/panel-cancellation.test.js.map +1 -1
- package/dist/panel-cli-ui.test.d.ts +2 -0
- package/dist/panel-cli-ui.test.d.ts.map +1 -0
- package/dist/panel-cli-ui.test.js +91 -0
- package/dist/panel-cli-ui.test.js.map +1 -0
- package/dist/panel-cli.test.js +37 -39
- package/dist/panel-cli.test.js.map +1 -1
- package/dist/panel-config.d.ts +38 -0
- package/dist/panel-config.d.ts.map +1 -1
- package/dist/panel-config.js +135 -2
- package/dist/panel-config.js.map +1 -1
- package/dist/panel-config.test.js +79 -2
- package/dist/panel-config.test.js.map +1 -1
- package/dist/panel-loop.test.js +1 -1
- package/dist/panel-loop.test.js.map +1 -1
- package/dist/panel-tools.test.js +1 -1
- package/dist/panel-tools.test.js.map +1 -1
- package/dist/panel-ui.d.ts +20 -0
- package/dist/panel-ui.d.ts.map +1 -0
- package/dist/panel-ui.js +84 -0
- package/dist/panel-ui.js.map +1 -0
- package/dist/panel-ui.test.d.ts +2 -0
- package/dist/panel-ui.test.d.ts.map +1 -0
- package/dist/panel-ui.test.js +122 -0
- package/dist/panel-ui.test.js.map +1 -0
- package/dist/panel-view.d.ts +1 -0
- package/dist/panel-view.d.ts.map +1 -1
- package/dist/panel-view.js +40 -6
- package/dist/panel-view.js.map +1 -1
- package/dist/panel-view.test.js +68 -1
- package/dist/panel-view.test.js.map +1 -1
- package/dist/panel.d.ts +11 -1
- package/dist/panel.d.ts.map +1 -1
- package/dist/panel.js +67 -21
- package/dist/panel.js.map +1 -1
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +34 -3
- package/dist/prompt.js.map +1 -1
- package/dist/prompt.test.js +21 -2
- package/dist/prompt.test.js.map +1 -1
- package/dist/review-events.test.js +1 -1
- package/dist/review-events.test.js.map +1 -1
- package/dist/review-result.test.js +1 -1
- package/dist/review-result.test.js.map +1 -1
- package/dist/review.js +3 -3
- package/dist/review.js.map +1 -1
- package/dist/review.meta.test.js +1 -1
- package/dist/review.meta.test.js.map +1 -1
- package/dist/skill-protocol.test.js +1 -1
- package/dist/skill-protocol.test.js.map +1 -1
- package/dist/skill-update.test.d.ts +2 -0
- package/dist/skill-update.test.d.ts.map +1 -0
- package/dist/skill-update.test.js +23 -0
- package/dist/skill-update.test.js.map +1 -0
- package/dist/skill.d.ts +22 -0
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +109 -22
- package/dist/skill.js.map +1 -1
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui-client.d.ts +13 -0
- package/dist/ui-client.d.ts.map +1 -0
- package/dist/ui-client.js +161 -0
- package/dist/ui-client.js.map +1 -0
- package/dist/ui-client.test.d.ts +2 -0
- package/dist/ui-client.test.d.ts.map +1 -0
- package/dist/ui-client.test.js +48 -0
- package/dist/ui-client.test.js.map +1 -0
- package/dist/ui-html.d.ts +7 -0
- package/dist/ui-html.d.ts.map +1 -0
- package/dist/ui-html.js +52 -0
- package/dist/ui-html.js.map +1 -0
- package/dist/ui-html.test.d.ts +2 -0
- package/dist/ui-html.test.d.ts.map +1 -0
- package/dist/ui-html.test.js +26 -0
- package/dist/ui-html.test.js.map +1 -0
- package/dist/ui-server-main.d.ts +3 -0
- package/dist/ui-server-main.d.ts.map +1 -0
- package/dist/ui-server-main.js +128 -0
- package/dist/ui-server-main.js.map +1 -0
- package/dist/ui-server-main.test.d.ts +2 -0
- package/dist/ui-server-main.test.d.ts.map +1 -0
- package/dist/ui-server-main.test.js +123 -0
- package/dist/ui-server-main.test.js.map +1 -0
- package/dist/ui-server.d.ts +42 -0
- package/dist/ui-server.d.ts.map +1 -0
- package/dist/ui-server.js +233 -0
- package/dist/ui-server.js.map +1 -0
- package/dist/ui-server.test.d.ts +2 -0
- package/dist/ui-server.test.d.ts.map +1 -0
- package/dist/ui-server.test.js +236 -0
- package/dist/ui-server.test.js.map +1 -0
- package/dist/update.d.ts +2 -0
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +80 -20
- package/dist/update.js.map +1 -1
- package/dist/update.test.d.ts +2 -0
- package/dist/update.test.d.ts.map +1 -0
- package/dist/update.test.js +17 -0
- package/dist/update.test.js.map +1 -0
- package/extensions/panel-tool.test.ts +366 -8
- package/extensions/panel-tool.ts +289 -68
- package/extensions/review.test.ts +79 -0
- package/extensions/review.ts +188 -44
- package/extensions/rv-completions.test.ts +177 -18
- package/extensions/rv-completions.ts +490 -60
- package/extensions/rv-interactive.test.ts +562 -0
- package/extensions/rv-interactive.ts +616 -0
- package/extensions/rv-locale.test.ts +12 -4
- package/extensions/rv-locale.ts +44 -11
- package/extensions/rv-model-picker.test.ts +172 -0
- package/extensions/rv-model-picker.ts +313 -0
- package/extensions/rv-model-priorities.test.ts +29 -9
- package/extensions/rv-model-priorities.ts +23 -8
- package/extensions/rv-prompts.test.ts +274 -2
- package/extensions/rv-prompts.ts +479 -33
- package/extensions/rv-resolve.test.ts +189 -0
- package/extensions/rv-resolve.ts +313 -0
- package/extensions/rv-semantic.test.ts +1 -1
- package/extensions/rv-semantic.ts +38 -4
- package/package.json +7 -4
- package/resources/rv-model-priorities.json +18 -9
- package/skills/pi-review/SKILL.md +84 -60
- package/skills/pi-review/references/codex-tools.md +18 -11
- package/skills/pi-review/references/model-selection.md +49 -28
- package/dist/extensions/review.d.ts +0 -2
- package/dist/extensions/review.js +0 -22
- package/dist/extensions/rv-prompts.d.ts +0 -19
- package/dist/extensions/rv-prompts.js +0 -132
package/extensions/review.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
2
3
|
import {
|
|
3
4
|
buildRvCompletions,
|
|
4
5
|
type ModelInfo,
|
|
@@ -7,20 +8,31 @@ import { detectRvLocale } from "./rv-locale.js";
|
|
|
7
8
|
import {
|
|
8
9
|
buildRvOrchestrationPrompt,
|
|
9
10
|
parseRvArgs,
|
|
10
|
-
RV_COMPLETIONS,
|
|
11
11
|
validateRvParsed,
|
|
12
|
+
type RvStrategy,
|
|
12
13
|
} from "./rv-prompts.js";
|
|
14
|
+
import { resolveRvParsed } from "./rv-resolve.js";
|
|
15
|
+
import {
|
|
16
|
+
runRvInteractiveWizard,
|
|
17
|
+
shouldRunInteractiveWizard,
|
|
18
|
+
stripInteractiveToken,
|
|
19
|
+
} from "./rv-interactive.js";
|
|
20
|
+
import { RvModelPickerComponent, type ModelPickerResult } from "./rv-model-picker.js";
|
|
13
21
|
import { registerPanelReviewTool } from "./panel-tool.js";
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* Captured at `session_start` so the synchronous-ish `getArgumentCompletions`
|
|
17
25
|
* callback (which receives only the argument prefix) can read the live model
|
|
18
|
-
* registry without ctx. Stays undefined in non-TUI/print mode;
|
|
19
|
-
*
|
|
26
|
+
* registry without ctx. Stays undefined in non-TUI/print mode; the pure
|
|
27
|
+
* completion builder then provides prefix-safe static suggestions.
|
|
20
28
|
*/
|
|
21
29
|
let capturedModels: ModelInfo[] | undefined;
|
|
22
30
|
let capturedPrimaryProvider: string | undefined;
|
|
23
|
-
let capturedLocale: ReturnType<typeof detectRvLocale> =
|
|
31
|
+
let capturedLocale: ReturnType<typeof detectRvLocale> = detectRvLocale([]);
|
|
32
|
+
/** Keep a registry handle so completions can refresh models lazily if session_start saw none yet. */
|
|
33
|
+
let capturedModelRegistry: { getAvailable?: () => unknown[] } | undefined;
|
|
34
|
+
/** Keep session manager so locale can re-detect on each completion as Chinese messages arrive. */
|
|
35
|
+
let capturedSessionManager: { getEntries?: () => Array<{ type?: string; content?: unknown }> } | undefined;
|
|
24
36
|
|
|
25
37
|
function sampleSessionText(
|
|
26
38
|
sessionManager: { getEntries?: () => Array<{ type?: string; content?: unknown }> },
|
|
@@ -68,68 +80,200 @@ function toModelInfo(m: {
|
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
function refreshCapturedModels(
|
|
84
|
+
registry?: { getAvailable?: () => unknown[] },
|
|
85
|
+
primaryProvider?: string,
|
|
86
|
+
): ModelInfo[] {
|
|
87
|
+
if (registry) capturedModelRegistry = registry;
|
|
88
|
+
if (primaryProvider) capturedPrimaryProvider = primaryProvider;
|
|
89
|
+
try {
|
|
90
|
+
const available = (capturedModelRegistry?.getAvailable?.() ?? []) as Array<Parameters<typeof toModelInfo>[0]>;
|
|
91
|
+
if (available.length > 0) {
|
|
92
|
+
capturedModels = available.map((m) => toModelInfo(m));
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// keep previous capture
|
|
96
|
+
}
|
|
97
|
+
return capturedModels ?? [];
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
export default function piReviewExtension(pi: ExtensionAPI) {
|
|
72
101
|
registerPanelReviewTool(pi);
|
|
73
102
|
pi.on("session_start", (_event, ctx) => {
|
|
74
103
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
// Model availability is session-scoped. Clear the previous snapshot before
|
|
105
|
+
// the new registry is sampled so an empty/cold catalog cannot leak stale IDs.
|
|
106
|
+
capturedModels = undefined;
|
|
107
|
+
capturedModelRegistry = ctx.modelRegistry;
|
|
77
108
|
capturedPrimaryProvider = ctx.model?.provider;
|
|
109
|
+
capturedSessionManager = ctx.sessionManager;
|
|
78
110
|
capturedLocale = detectRvLocale(sampleSessionText(ctx.sessionManager));
|
|
111
|
+
refreshCapturedModels(ctx.modelRegistry, ctx.model?.provider);
|
|
79
112
|
} catch {
|
|
80
113
|
capturedModels = undefined;
|
|
81
114
|
capturedPrimaryProvider = undefined;
|
|
82
|
-
capturedLocale =
|
|
115
|
+
capturedLocale = detectRvLocale([]);
|
|
83
116
|
}
|
|
84
117
|
});
|
|
85
118
|
|
|
86
|
-
function localeForHandler(ctx
|
|
119
|
+
function localeForHandler(ctx?: { sessionManager?: Parameters<typeof sampleSessionText>[0] }): typeof capturedLocale {
|
|
87
120
|
try {
|
|
88
|
-
|
|
121
|
+
const manager = ctx?.sessionManager ?? capturedSessionManager ?? { getEntries: () => [] };
|
|
122
|
+
capturedLocale = detectRvLocale(sampleSessionText(manager));
|
|
123
|
+
return capturedLocale;
|
|
89
124
|
} catch {
|
|
90
125
|
return capturedLocale;
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
128
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
function argumentCompletions(
|
|
130
|
+
prefix: string,
|
|
131
|
+
strategy: RvStrategy = "panel",
|
|
132
|
+
): ReturnType<typeof buildRvCompletions> {
|
|
133
|
+
// Always try a lazy refresh. session_start can race before providers finish loading.
|
|
134
|
+
const models = refreshCapturedModels();
|
|
135
|
+
// Re-detect locale every completion so Chinese sessions don't stay stuck on English labels.
|
|
136
|
+
const locale = localeForHandler();
|
|
137
|
+
return buildRvCompletions(prefix, {
|
|
138
|
+
models,
|
|
139
|
+
primaryProvider: capturedPrimaryProvider,
|
|
140
|
+
locale,
|
|
141
|
+
strategy,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function handleRvCommand(
|
|
146
|
+
strategy: RvStrategy,
|
|
147
|
+
rawArgs: string,
|
|
148
|
+
ctx: {
|
|
149
|
+
ui: {
|
|
150
|
+
notify: (message: string, level?: "warning" | "info" | "error") => void;
|
|
151
|
+
select?: (title: string, options: string[]) => Promise<string | undefined>;
|
|
152
|
+
input?: (title: string, placeholder?: string) => Promise<string | undefined>;
|
|
153
|
+
confirm?: (title: string, message: string) => Promise<boolean>;
|
|
154
|
+
custom?: <T>(
|
|
155
|
+
factory: (
|
|
156
|
+
tui: TUI,
|
|
157
|
+
theme: Theme,
|
|
158
|
+
keybindings: KeybindingsManager,
|
|
159
|
+
done: (result: T) => void,
|
|
160
|
+
) => Component & { dispose?(): void },
|
|
161
|
+
options?: { overlay?: boolean },
|
|
162
|
+
) => Promise<T>;
|
|
163
|
+
};
|
|
164
|
+
sessionManager?: Parameters<typeof sampleSessionText>[0];
|
|
112
165
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (strategy === "models") {
|
|
168
|
+
const parsed = parseRvArgs("", "models");
|
|
169
|
+
pi.sendUserMessage(buildRvOrchestrationPrompt(parsed, localeForHandler(ctx)));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
173
|
+
const interactiveRequested = shouldRunInteractiveWizard(rawArgs, parseRvArgs(stripInteractiveToken(rawArgs), strategy));
|
|
174
|
+
const trimmed = stripInteractiveToken(rawArgs).trim();
|
|
175
|
+
let parsed = parseRvArgs(trimmed, strategy);
|
|
176
|
+
|
|
177
|
+
// Interactive wizard: empty command or explicit `interactive` / `--interactive` / `-i`.
|
|
178
|
+
// Uses Pi select/input/confirm dialogs — no tab completion required for model assignment.
|
|
179
|
+
if (interactiveRequested && ctx.ui.select && ctx.ui.input && ctx.ui.confirm) {
|
|
180
|
+
const models = refreshCapturedModels();
|
|
181
|
+
const locale = localeForHandler(ctx);
|
|
182
|
+
const wizardResult = await runRvInteractiveWizard(
|
|
183
|
+
{
|
|
184
|
+
select: (title, options) => ctx.ui.select!(title, options),
|
|
185
|
+
input: (title, placeholder) => ctx.ui.input!(title, placeholder),
|
|
186
|
+
confirm: (title, message) => ctx.ui.confirm!(title, message),
|
|
187
|
+
notify: (message, type) => ctx.ui.notify(message, type),
|
|
188
|
+
// Inline searchable picker, available only in TUI mode (ctx.ui.custom).
|
|
189
|
+
// The factory builds the component; the host grants focus and forwards
|
|
190
|
+
// keystrokes. print/RPC modes have no `custom` → wizard falls back to
|
|
191
|
+
// the select-based flow.
|
|
192
|
+
customModelPicker: ctx.ui.custom
|
|
193
|
+
? (pickerInput) =>
|
|
194
|
+
ctx.ui.custom!<ModelPickerResult>((tui, theme, keybindings, done) =>
|
|
195
|
+
RvModelPickerComponent.create(
|
|
196
|
+
tui,
|
|
197
|
+
theme as unknown as Theme,
|
|
198
|
+
keybindings,
|
|
199
|
+
done,
|
|
200
|
+
pickerInput,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
: undefined,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
strategy: strategy === "loop" ? "loop" : "panel",
|
|
207
|
+
seed: parsed,
|
|
208
|
+
models,
|
|
209
|
+
locale,
|
|
210
|
+
primaryProvider: capturedPrimaryProvider,
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
if (!wizardResult) return;
|
|
214
|
+
parsed = wizardResult;
|
|
215
|
+
} else if (!trimmed) {
|
|
216
|
+
// No dialog API (print/RPC) and empty args → tell user how to run interactive.
|
|
217
|
+
ctx.ui.notify(
|
|
218
|
+
strategy === "loop"
|
|
219
|
+
? "/rv-loop interactive · or /rv-loop --reviewers 3 --reviewer-model r1=... @src"
|
|
220
|
+
: "/rv interactive · or /rv --panel code-experts @src",
|
|
221
|
+
"warning",
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const validation = validateRvParsed(parsed);
|
|
227
|
+
if (!validation.ok) {
|
|
228
|
+
ctx.ui.notify(validation.message, "warning");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!parsed.modelsOnly && !parsed.target && !parsed.continueHandle) {
|
|
233
|
+
ctx.ui.notify("/rv needs a natural-language target, --continue <handle>, interactive, or /rv-models.", "warning");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
126
236
|
|
|
127
|
-
|
|
128
|
-
|
|
237
|
+
const resolved = resolveRvParsed(parsed, refreshCapturedModels(), capturedPrimaryProvider);
|
|
238
|
+
if (resolved.ambiguousModels?.length) {
|
|
239
|
+
// Prefer interactive disambiguation when dialogs exist.
|
|
240
|
+
if (ctx.ui.select) {
|
|
241
|
+
const pick = await ctx.ui.select(
|
|
242
|
+
`Ambiguous model "${parsed.model}"`,
|
|
243
|
+
resolved.ambiguousModels,
|
|
244
|
+
);
|
|
245
|
+
if (!pick) return;
|
|
246
|
+
resolved.parsed = { ...resolved.parsed, model: pick };
|
|
247
|
+
resolved.notes.push(`model ${parsed.model} → ${pick} (interactive pick)`);
|
|
248
|
+
} else {
|
|
249
|
+
ctx.ui.notify(
|
|
250
|
+
`Model "${parsed.model}" is ambiguous. Candidates: ${resolved.ambiguousModels.join(", ")}. Re-run with an exact provider/model.`,
|
|
251
|
+
"warning",
|
|
252
|
+
);
|
|
129
253
|
return;
|
|
130
254
|
}
|
|
255
|
+
}
|
|
131
256
|
|
|
132
|
-
|
|
133
|
-
|
|
257
|
+
pi.sendUserMessage(buildRvOrchestrationPrompt(resolved.parsed, localeForHandler(ctx), resolved.notes));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
pi.registerCommand("rv", {
|
|
261
|
+
description:
|
|
262
|
+
"Panel review. /rv interactive for dialog wizard; or /rv [flags] <natural-language target>",
|
|
263
|
+
getArgumentCompletions: (prefix) => argumentCompletions(prefix, "panel"),
|
|
264
|
+
handler: async (rawArgs, ctx) => handleRvCommand("panel", rawArgs, ctx),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
pi.registerCommand("rv-loop", {
|
|
268
|
+
description:
|
|
269
|
+
"Loop closeout. /rv-loop interactive for dialog wizard (pick reviewers & models with arrows); or pass flags",
|
|
270
|
+
getArgumentCompletions: (prefix) => argumentCompletions(prefix, "loop"),
|
|
271
|
+
handler: async (rawArgs, ctx) => handleRvCommand("loop", rawArgs, ctx),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
pi.registerCommand("rv-models", {
|
|
275
|
+
description: "List pi-review models. Usage: /rv-models",
|
|
276
|
+
getArgumentCompletions: (prefix) => argumentCompletions(prefix, "models"),
|
|
277
|
+
handler: async (_rawArgs, ctx) => handleRvCommand("models", "", ctx),
|
|
134
278
|
});
|
|
135
279
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
buildRvCompletions,
|
|
5
5
|
extractSignals,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./rv-completions.js";
|
|
12
12
|
import { rvUi } from "./rv-locale.js";
|
|
13
13
|
import { DEFAULT_REVIEW_MODEL_PRIORITIES } from "./rv-model-priorities.js";
|
|
14
|
+
import { parseRvArgs, validateRvParsed } from "./rv-prompts.js";
|
|
14
15
|
|
|
15
16
|
function model(partial: Partial<ModelInfo> & Pick<ModelInfo, "provider" | "id">): ModelInfo {
|
|
16
17
|
return {
|
|
@@ -24,7 +25,13 @@ function model(partial: Partial<ModelInfo> & Pick<ModelInfo, "provider" | "id">)
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const MODELS: ModelInfo[] = [
|
|
27
|
-
model({ provider: "
|
|
28
|
+
model({ provider: "openai-codex", id: "gpt-5.6-sol", reasoning: true, contextWindow: 400_000, thinkingLevels: ["off", "minimal", "low", "medium", "high", "xhigh"] }),
|
|
29
|
+
model({ provider: "openai-codex", id: "gpt-5.6-terra", reasoning: true, contextWindow: 400_000, thinkingLevels: ["off", "low", "medium", "high", "xhigh"] }),
|
|
30
|
+
model({ provider: "openai-codex", id: "gpt-5.6-luna", reasoning: true, contextWindow: 400_000, thinkingLevels: ["high", "xhigh"] }),
|
|
31
|
+
model({ provider: "anthropic", id: "claude-opus-4-8", reasoning: true, contextWindow: 200_000, thinkingLevels: ["off", "low", "medium", "high", "xhigh"] }),
|
|
32
|
+
model({ provider: "anthropic", id: "claude-sonnet-4-5", reasoning: true, contextWindow: 200_000, thinkingLevels: ["off", "low", "medium", "high", "xhigh"] }),
|
|
33
|
+
model({ provider: "anthropic", id: "claude-fable-5", reasoning: true, contextWindow: 200_000, thinkingLevels: ["high", "xhigh"] }),
|
|
34
|
+
model({ provider: "deepseek", id: "deepseek-v4-flash", reasoning: true, contextWindow: 200_000, thinkingLevels: ["high", "xhigh"] }),
|
|
28
35
|
model({ provider: "openai", id: "gpt-5.5", reasoning: true, contextWindow: 400_000, thinkingLevels: ["off", "minimal", "low", "medium", "high", "xhigh"] }),
|
|
29
36
|
model({ provider: "zenmux", id: "gemini-3-flash", reasoning: false, contextWindow: 1_000_000, thinkingLevels: [] }),
|
|
30
37
|
];
|
|
@@ -80,16 +87,16 @@ describe("extractSignals", () => {
|
|
|
80
87
|
});
|
|
81
88
|
|
|
82
89
|
describe("rankModelsForReview", () => {
|
|
83
|
-
it("puts
|
|
90
|
+
it("puts fast-review models first for code presets", () => {
|
|
84
91
|
const sig = extractSignals(["@src/a.ts"], {});
|
|
85
92
|
const ranked = rankModelsForReview(MODELS, sig, DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
86
|
-
assert.equal(ranked[0].id, "
|
|
93
|
+
assert.equal(ranked[0].id, "claude-sonnet-4-5");
|
|
87
94
|
});
|
|
88
95
|
|
|
89
|
-
it("puts
|
|
96
|
+
it("puts complex/plan models first for plan mode", () => {
|
|
90
97
|
const sig = extractSignals(["--mode", "plan"], {});
|
|
91
98
|
const ranked = rankModelsForReview(MODELS, sig, DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
92
|
-
assert.equal(ranked[0].id, "
|
|
99
|
+
assert.equal(ranked[0].id, "gpt-5.6-sol");
|
|
93
100
|
});
|
|
94
101
|
});
|
|
95
102
|
|
|
@@ -125,12 +132,12 @@ describe("buildRvCompletions", () => {
|
|
|
125
132
|
});
|
|
126
133
|
|
|
127
134
|
it("completes thinking suffix after provider/id: for a reasoning model", () => {
|
|
128
|
-
const items = buildRvCompletions("--model openai/gpt-5.
|
|
135
|
+
const items = buildRvCompletions("--model openai-codex/gpt-5.6-sol:", deps);
|
|
129
136
|
assert.ok(items);
|
|
130
137
|
const labels = items!.map((i) => i.label);
|
|
131
138
|
assert.ok(labels.includes("high"));
|
|
132
139
|
assert.ok(labels.includes("xhigh"));
|
|
133
|
-
assert.ok(items!.every((i) => i.value.startsWith("--model openai/gpt-5.
|
|
140
|
+
assert.ok(items!.every((i) => i.value.startsWith("--model openai-codex/gpt-5.6-sol:")));
|
|
134
141
|
});
|
|
135
142
|
|
|
136
143
|
it("does not offer thinking suffix for a non-reasoning model", () => {
|
|
@@ -167,14 +174,151 @@ describe("buildRvCompletions", () => {
|
|
|
167
174
|
assert.equal(buildRvCompletions("--continue abc", deps), null);
|
|
168
175
|
});
|
|
169
176
|
|
|
170
|
-
it("
|
|
177
|
+
it("empty top-level stays quiet: flags/hints, no preset wall", () => {
|
|
171
178
|
const items = buildRvCompletions("", deps);
|
|
172
179
|
const ui = rvUi("en");
|
|
173
180
|
assert.ok(items);
|
|
174
|
-
assert.ok(items!.some((i) => i.
|
|
175
|
-
assert.ok(items!.some((i) => i.label === ui.
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
assert.ok(items!.some((i) => i.value === "-i" || i.label.includes("interactive")));
|
|
182
|
+
assert.ok(items!.some((i) => i.label === ui.presetHint));
|
|
183
|
+
assert.ok(!items!.some((i) => i.label === ui.codePreset));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("-i / --interactive complete to the wizard trigger, not the flag soup", () => {
|
|
187
|
+
for (const prefix of ["-i", "--interactive", "-"]) {
|
|
188
|
+
const items = buildRvCompletions(prefix, { ...deps, strategy: "loop" });
|
|
189
|
+
assert.ok(items?.some((i) => i.value === "-i" || i.value === "--interactive"), prefix);
|
|
190
|
+
assert.ok(!items?.some((i) => i.label === "--max-rounds 1" || i.value.startsWith("--max-rounds 1")), prefix);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("presets appear only when the user asks for them", () => {
|
|
195
|
+
const ui = rvUi("en");
|
|
196
|
+
const quiet = buildRvCompletions("gpt", deps);
|
|
197
|
+
assert.ok(!quiet?.some((i) => i.label === ui.codePreset));
|
|
198
|
+
const asked = buildRvCompletions("preset", deps);
|
|
199
|
+
assert.ok(asked?.some((i) => i.label === ui.codePreset), `got ${asked?.map((i) => i.label).join(",")}`);
|
|
200
|
+
const zh = buildRvCompletions("预设", { ...deps, locale: "zh" });
|
|
201
|
+
assert.ok(zh?.some((i) => i.label === rvUi("zh").codePreset), `got ${zh?.map((i) => i.label).join(",")}`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("/rv-loop empty top-level stays quiet; loop presets need explicit request", () => {
|
|
205
|
+
const empty = buildRvCompletions("", { ...deps, strategy: "loop" });
|
|
206
|
+
assert.ok(empty);
|
|
207
|
+
assert.ok(!empty!.some((i) => /Loop closeout|关单/.test(i.label)));
|
|
208
|
+
assert.ok(!empty!.some((i) => i.value.includes("--keep-session")));
|
|
209
|
+
const asked = buildRvCompletions("preset", { ...deps, strategy: "loop" });
|
|
210
|
+
assert.ok(asked?.some((i) => /Loop closeout/.test(i.label)));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("Chinese locale labels are used when locale=zh", () => {
|
|
214
|
+
const items = buildRvCompletions("预设", { ...deps, locale: "zh" });
|
|
215
|
+
const ui = rvUi("zh");
|
|
216
|
+
assert.ok(items?.some((i) => i.label === ui.codePreset));
|
|
217
|
+
assert.ok(items?.some((i) => i.description?.includes("--model")));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("/rv-loop bare model typing surfaces catalog matches as --model values", () => {
|
|
221
|
+
const items = buildRvCompletions("gpt-5.6-sol", { ...deps, strategy: "loop" });
|
|
222
|
+
assert.ok(items);
|
|
223
|
+
assert.ok(items!.some((i) => i.value.includes("--model") && i.label.includes("gpt-5.6-sol")));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("/rv-loop accepts full provider/model and offers thinking suffixes", () => {
|
|
227
|
+
const items = buildRvCompletions("openai-codex/gpt-5.6-sol", { ...deps, strategy: "loop" });
|
|
228
|
+
assert.ok(items);
|
|
229
|
+
assert.ok(items!.some((i) => i.value === "--model openai-codex/gpt-5.6-sol"));
|
|
230
|
+
assert.ok(items!.some((i) => i.value.startsWith("--model openai-codex/gpt-5.6-sol:")));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("/rv-loop provider fragment still surfaces models", () => {
|
|
234
|
+
const items = buildRvCompletions("openai-codex", { ...deps, strategy: "loop" });
|
|
235
|
+
assert.ok(items);
|
|
236
|
+
assert.ok(items!.some((i) => i.label.includes("openai-codex/") || i.value.includes("openai-codex/")));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("/rv-models does not offer panel scene templates or targets", () => {
|
|
240
|
+
const items = buildRvCompletions("", { ...deps, strategy: "models" });
|
|
241
|
+
assert.ok(items);
|
|
242
|
+
assert.ok(items!.every((i) => /model/i.test(i.label)));
|
|
243
|
+
assert.ok(!items!.some((i) => i.value.includes("@src") || i.value.includes("--model")));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("/rv-models completion never clears an already-entered prefix", () => {
|
|
247
|
+
for (const prefix of ["unexpected target ", "unexpected target mod"]) {
|
|
248
|
+
const items = buildRvCompletions(prefix, { models: [], strategy: "models" });
|
|
249
|
+
assert.ok(items, prefix);
|
|
250
|
+
assert.ok(items!.every((item) => item.value.startsWith(prefix)), prefix);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("multi-word semantic completion replaces its typed phrase suffix without duplicating it", () => {
|
|
255
|
+
for (const prefix of ["@src code r", "@src code rev"]) {
|
|
256
|
+
const items = buildRvCompletions(prefix, { ...deps, strategy: "panel", locale: "en" });
|
|
257
|
+
const codeReview = items?.find((item) => item.label === "code review");
|
|
258
|
+
assert.ok(codeReview, prefix);
|
|
259
|
+
assert.equal(codeReview!.value, "@src code review");
|
|
260
|
+
assert.doesNotMatch(codeReview!.value, /code code review/);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("/rv-loop flag completion includes --max-rounds and excludes --keep-session", () => {
|
|
265
|
+
const maxRounds = buildRvCompletions("--max", { ...deps, strategy: "loop" });
|
|
266
|
+
assert.ok(maxRounds?.some((i) => i.label === "--max-rounds"));
|
|
267
|
+
const keep = buildRvCompletions("--keep", { ...deps, strategy: "loop" });
|
|
268
|
+
assert.ok(!keep?.some((i) => i.label === "--keep-session"));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("/rv-loop completes --reviewers values and panel strategy flags", () => {
|
|
272
|
+
const flags = buildRvCompletions("--rev", { ...deps, strategy: "loop" });
|
|
273
|
+
assert.ok(flags?.some((i) => i.label === "--reviewers"), `got ${flags?.map((i) => i.label)}`);
|
|
274
|
+
const values = buildRvCompletions("--reviewers ", { ...deps, strategy: "loop" });
|
|
275
|
+
assert.ok(values?.some((i) => i.label === "3"));
|
|
276
|
+
const consensus = buildRvCompletions("--consensus ", { ...deps, strategy: "loop" });
|
|
277
|
+
assert.ok(consensus?.some((i) => i.label === "quorum"));
|
|
278
|
+
const panel = buildRvCompletions("--panel ", { ...deps, strategy: "loop" });
|
|
279
|
+
assert.ok(panel?.some((i) => i.label === "code-experts"));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("offers an explicit target boundary after completed flags so target text can be entered last", () => {
|
|
283
|
+
const prefix = "--mode plan --panel code-experts ";
|
|
284
|
+
const items = buildRvCompletions(prefix, { ...deps, strategy: "panel", locale: "en" });
|
|
285
|
+
const boundary = items?.find((item) => item.label === "Start review target");
|
|
286
|
+
assert.ok(boundary, `got ${items?.map((item) => item.label).join(", ")}`);
|
|
287
|
+
assert.equal(boundary!.value, "--mode plan --panel code-experts -- ");
|
|
288
|
+
|
|
289
|
+
const parsed = parseRvArgs(`${boundary!.value}code review the auth changes under @src`);
|
|
290
|
+
assert.equal(parsed.mode, "plan");
|
|
291
|
+
assert.equal(parsed.panel, "code-experts");
|
|
292
|
+
assert.equal(parsed.target, "code review the auth changes under @src");
|
|
293
|
+
assert.deepEqual(validateRvParsed(parsed), { ok: true });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("hides --panel after --reviewers and vice versa", () => {
|
|
297
|
+
const afterReviewers = buildRvCompletions("--reviewers 3 --p", { ...deps, strategy: "loop" });
|
|
298
|
+
assert.ok(!afterReviewers?.some((i) => i.label === "--panel"));
|
|
299
|
+
const afterPanel = buildRvCompletions("--panel code-experts --r", { ...deps, strategy: "loop" });
|
|
300
|
+
assert.ok(!afterPanel?.some((i) => i.label === "--reviewers"));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("--reviewer-model offers cascading id then model menus based on --reviewers count", () => {
|
|
304
|
+
const ids = buildRvCompletions("--reviewers 3 --reviewer-model ", { ...deps, strategy: "loop" });
|
|
305
|
+
assert.deepEqual(ids?.map((i) => i.label).sort(), ["r1=", "r2=", "r3="].sort());
|
|
306
|
+
|
|
307
|
+
const models = buildRvCompletions("--reviewers 3 --reviewer-model r1=", { ...deps, strategy: "loop" });
|
|
308
|
+
assert.ok(models?.some((i) => i.label.startsWith("r1=") && i.label.includes("/")), `got ${models?.map((i) => i.label)}`);
|
|
309
|
+
|
|
310
|
+
const afterOne = buildRvCompletions(
|
|
311
|
+
"--reviewers 2 --reviewer-model r1=openai-codex/gpt-5.6-sol --reviewer-model ",
|
|
312
|
+
{ ...deps, strategy: "loop" },
|
|
313
|
+
);
|
|
314
|
+
assert.ok(afterOne?.some((i) => i.label === "r2="));
|
|
315
|
+
assert.ok(!afterOne?.some((i) => i.label === "r1="));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("--reviewer-model with --panel code-experts uses role ids", () => {
|
|
319
|
+
const ids = buildRvCompletions("--panel code-experts --reviewer-model ", { ...deps, strategy: "panel" });
|
|
320
|
+
assert.ok(ids?.some((i) => i.label === "security="));
|
|
321
|
+
assert.ok(ids?.some((i) => i.label === "correctness="));
|
|
178
322
|
});
|
|
179
323
|
|
|
180
324
|
it("does not duplicate list-models completion at top level", () => {
|
|
@@ -218,11 +362,15 @@ describe("buildRvCompletions", () => {
|
|
|
218
362
|
});
|
|
219
363
|
|
|
220
364
|
it("omits preset scene templates when registry has no preset id match", () => {
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
365
|
+
// Only a non-matching flash model: none of the code/plan/frontend preset needles hit.
|
|
366
|
+
const nonMatching = [
|
|
367
|
+
model({ provider: "zenmux", id: "gemini-3-flash", reasoning: false, contextWindow: 1_000_000, thinkingLevels: [] }),
|
|
368
|
+
];
|
|
369
|
+
const empty = buildRvCompletions("", { models: nonMatching, priorities: DEFAULT_REVIEW_MODEL_PRIORITIES });
|
|
370
|
+
assert.ok(empty);
|
|
371
|
+
assert.ok(!empty!.some((i) => i.label === rvUi("en").codePreset));
|
|
372
|
+
const asked = buildRvCompletions("preset", { models: nonMatching, priorities: DEFAULT_REVIEW_MODEL_PRIORITIES });
|
|
373
|
+
assert.ok(!asked?.some((i) => i.label === rvUi("en").codePreset));
|
|
226
374
|
});
|
|
227
375
|
|
|
228
376
|
it("falls back gracefully when models are unavailable (static flags still work)", () => {
|
|
@@ -230,4 +378,15 @@ describe("buildRvCompletions", () => {
|
|
|
230
378
|
assert.ok(items);
|
|
231
379
|
assert.ok(items!.some((i) => i.label === "code" || i.label === "plan" || i.label === "challenge"));
|
|
232
380
|
});
|
|
381
|
+
|
|
382
|
+
it("preserves entered targets when the model catalog is unavailable", () => {
|
|
383
|
+
const valuePosition = buildRvCompletions("@src --model ", { models: [], strategy: "panel" });
|
|
384
|
+
assert.ok(valuePosition);
|
|
385
|
+
assert.ok(valuePosition!.every((item) => item.value.startsWith("@src --model ")));
|
|
386
|
+
|
|
387
|
+
const bareModel = buildRvCompletions("@src gpt-5.6", { models: [], strategy: "panel" });
|
|
388
|
+
assert.ok(bareModel);
|
|
389
|
+
assert.ok(bareModel!.some((item) => item.value === "@src --model gpt-5.6"));
|
|
390
|
+
assert.ok(bareModel!.every((item) => item.value.startsWith("@src ")));
|
|
391
|
+
});
|
|
233
392
|
});
|