@zephyrdeng/pi-review 0.7.0 → 0.8.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.
Files changed (194) hide show
  1. package/README.md +51 -7
  2. package/README.zh-CN.md +45 -5
  3. package/dist/args.d.ts +4 -0
  4. package/dist/args.d.ts.map +1 -1
  5. package/dist/args.js +82 -5
  6. package/dist/args.js.map +1 -1
  7. package/dist/args.test.js +72 -1
  8. package/dist/args.test.js.map +1 -1
  9. package/dist/child-process.d.ts +6 -0
  10. package/dist/child-process.d.ts.map +1 -1
  11. package/dist/child-process.js +48 -0
  12. package/dist/child-process.js.map +1 -1
  13. package/dist/child-process.test.js +55 -1
  14. package/dist/child-process.test.js.map +1 -1
  15. package/dist/cli-exit.test.js +14 -5
  16. package/dist/cli-exit.test.js.map +1 -1
  17. package/dist/cli.js +8 -3
  18. package/dist/cli.js.map +1 -1
  19. package/dist/index.d.ts +13 -6
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/install-args.test.js +1 -1
  24. package/dist/install-args.test.js.map +1 -1
  25. package/dist/install.d.ts.map +1 -1
  26. package/dist/install.js +2 -3
  27. package/dist/install.js.map +1 -1
  28. package/dist/json-events.d.ts +34 -2
  29. package/dist/json-events.d.ts.map +1 -1
  30. package/dist/json-events.js +156 -49
  31. package/dist/json-events.js.map +1 -1
  32. package/dist/json-events.test.js +176 -4
  33. package/dist/json-events.test.js.map +1 -1
  34. package/dist/loop.d.ts +28 -2
  35. package/dist/loop.d.ts.map +1 -1
  36. package/dist/loop.js +45 -3
  37. package/dist/loop.js.map +1 -1
  38. package/dist/loop.test.js +24 -1
  39. package/dist/loop.test.js.map +1 -1
  40. package/dist/matcher.test.js +1 -1
  41. package/dist/matcher.test.js.map +1 -1
  42. package/dist/meta-footer.d.ts +4 -8
  43. package/dist/meta-footer.d.ts.map +1 -1
  44. package/dist/meta-footer.js +19 -5
  45. package/dist/meta-footer.js.map +1 -1
  46. package/dist/meta-footer.test.js +11 -4
  47. package/dist/meta-footer.test.js.map +1 -1
  48. package/dist/panel-aggregate.test.js +1 -1
  49. package/dist/panel-aggregate.test.js.map +1 -1
  50. package/dist/panel-cancellation.test.d.ts +2 -0
  51. package/dist/panel-cancellation.test.d.ts.map +1 -0
  52. package/dist/panel-cancellation.test.js +55 -0
  53. package/dist/panel-cancellation.test.js.map +1 -0
  54. package/dist/panel-cli-ui.test.d.ts +2 -0
  55. package/dist/panel-cli-ui.test.d.ts.map +1 -0
  56. package/dist/panel-cli-ui.test.js +91 -0
  57. package/dist/panel-cli-ui.test.js.map +1 -0
  58. package/dist/panel-cli.test.js +76 -27
  59. package/dist/panel-cli.test.js.map +1 -1
  60. package/dist/panel-config.d.ts +38 -0
  61. package/dist/panel-config.d.ts.map +1 -1
  62. package/dist/panel-config.js +137 -4
  63. package/dist/panel-config.js.map +1 -1
  64. package/dist/panel-config.test.js +84 -4
  65. package/dist/panel-config.test.js.map +1 -1
  66. package/dist/panel-loop.test.js +1 -1
  67. package/dist/panel-loop.test.js.map +1 -1
  68. package/dist/panel-tools.test.d.ts +2 -0
  69. package/dist/panel-tools.test.d.ts.map +1 -0
  70. package/dist/panel-tools.test.js +12 -0
  71. package/dist/panel-tools.test.js.map +1 -0
  72. package/dist/panel-ui.d.ts +20 -0
  73. package/dist/panel-ui.d.ts.map +1 -0
  74. package/dist/panel-ui.js +84 -0
  75. package/dist/panel-ui.js.map +1 -0
  76. package/dist/panel-ui.test.d.ts +2 -0
  77. package/dist/panel-ui.test.d.ts.map +1 -0
  78. package/dist/panel-ui.test.js +122 -0
  79. package/dist/panel-ui.test.js.map +1 -0
  80. package/dist/panel-usage.d.ts +6 -0
  81. package/dist/panel-usage.d.ts.map +1 -0
  82. package/dist/panel-usage.js +21 -0
  83. package/dist/panel-usage.js.map +1 -0
  84. package/dist/panel-view.d.ts +42 -0
  85. package/dist/panel-view.d.ts.map +1 -0
  86. package/dist/panel-view.js +124 -0
  87. package/dist/panel-view.js.map +1 -0
  88. package/dist/panel-view.test.d.ts +2 -0
  89. package/dist/panel-view.test.d.ts.map +1 -0
  90. package/dist/panel-view.test.js +112 -0
  91. package/dist/panel-view.test.js.map +1 -0
  92. package/dist/panel.d.ts +22 -2
  93. package/dist/panel.d.ts.map +1 -1
  94. package/dist/panel.js +184 -60
  95. package/dist/panel.js.map +1 -1
  96. package/dist/prompt.d.ts +2 -0
  97. package/dist/prompt.d.ts.map +1 -1
  98. package/dist/prompt.js +34 -3
  99. package/dist/prompt.js.map +1 -1
  100. package/dist/prompt.test.js +21 -2
  101. package/dist/prompt.test.js.map +1 -1
  102. package/dist/review-events.d.ts +70 -0
  103. package/dist/review-events.d.ts.map +1 -0
  104. package/dist/review-events.js +52 -0
  105. package/dist/review-events.js.map +1 -0
  106. package/dist/review-events.test.d.ts +2 -0
  107. package/dist/review-events.test.d.ts.map +1 -0
  108. package/dist/review-events.test.js +47 -0
  109. package/dist/review-events.test.js.map +1 -0
  110. package/dist/review-result.test.js +1 -1
  111. package/dist/review-result.test.js.map +1 -1
  112. package/dist/review.js +3 -3
  113. package/dist/review.js.map +1 -1
  114. package/dist/review.meta.test.js +1 -1
  115. package/dist/review.meta.test.js.map +1 -1
  116. package/dist/skill-protocol.test.js +1 -1
  117. package/dist/skill-protocol.test.js.map +1 -1
  118. package/dist/skill-update.test.d.ts +2 -0
  119. package/dist/skill-update.test.d.ts.map +1 -0
  120. package/dist/skill-update.test.js +23 -0
  121. package/dist/skill-update.test.js.map +1 -0
  122. package/dist/skill.d.ts +22 -0
  123. package/dist/skill.d.ts.map +1 -1
  124. package/dist/skill.js +109 -22
  125. package/dist/skill.js.map +1 -1
  126. package/dist/types.d.ts +28 -2
  127. package/dist/types.d.ts.map +1 -1
  128. package/dist/types.js +2 -0
  129. package/dist/types.js.map +1 -1
  130. package/dist/ui-client.d.ts +13 -0
  131. package/dist/ui-client.d.ts.map +1 -0
  132. package/dist/ui-client.js +161 -0
  133. package/dist/ui-client.js.map +1 -0
  134. package/dist/ui-client.test.d.ts +2 -0
  135. package/dist/ui-client.test.d.ts.map +1 -0
  136. package/dist/ui-client.test.js +48 -0
  137. package/dist/ui-client.test.js.map +1 -0
  138. package/dist/ui-html.d.ts +7 -0
  139. package/dist/ui-html.d.ts.map +1 -0
  140. package/dist/ui-html.js +52 -0
  141. package/dist/ui-html.js.map +1 -0
  142. package/dist/ui-html.test.d.ts +2 -0
  143. package/dist/ui-html.test.d.ts.map +1 -0
  144. package/dist/ui-html.test.js +26 -0
  145. package/dist/ui-html.test.js.map +1 -0
  146. package/dist/ui-server-main.d.ts +3 -0
  147. package/dist/ui-server-main.d.ts.map +1 -0
  148. package/dist/ui-server-main.js +128 -0
  149. package/dist/ui-server-main.js.map +1 -0
  150. package/dist/ui-server-main.test.d.ts +2 -0
  151. package/dist/ui-server-main.test.d.ts.map +1 -0
  152. package/dist/ui-server-main.test.js +123 -0
  153. package/dist/ui-server-main.test.js.map +1 -0
  154. package/dist/ui-server.d.ts +42 -0
  155. package/dist/ui-server.d.ts.map +1 -0
  156. package/dist/ui-server.js +233 -0
  157. package/dist/ui-server.js.map +1 -0
  158. package/dist/ui-server.test.d.ts +2 -0
  159. package/dist/ui-server.test.d.ts.map +1 -0
  160. package/dist/ui-server.test.js +236 -0
  161. package/dist/ui-server.test.js.map +1 -0
  162. package/dist/update.d.ts.map +1 -1
  163. package/dist/update.js +48 -19
  164. package/dist/update.js.map +1 -1
  165. package/extensions/panel-tool.test.ts +388 -0
  166. package/extensions/panel-tool.ts +390 -0
  167. package/extensions/review.test.ts +79 -0
  168. package/extensions/review.ts +191 -45
  169. package/extensions/rv-completions.test.ts +177 -18
  170. package/extensions/rv-completions.ts +490 -60
  171. package/extensions/rv-interactive.test.ts +562 -0
  172. package/extensions/rv-interactive.ts +616 -0
  173. package/extensions/rv-locale.test.ts +12 -4
  174. package/extensions/rv-locale.ts +44 -11
  175. package/extensions/rv-model-picker.test.ts +172 -0
  176. package/extensions/rv-model-picker.ts +313 -0
  177. package/extensions/rv-model-priorities.test.ts +29 -9
  178. package/extensions/rv-model-priorities.ts +23 -8
  179. package/extensions/rv-prompts.test.ts +281 -2
  180. package/extensions/rv-prompts.ts +480 -32
  181. package/extensions/rv-resolve.test.ts +189 -0
  182. package/extensions/rv-resolve.ts +313 -0
  183. package/extensions/rv-semantic.test.ts +1 -1
  184. package/extensions/rv-semantic.ts +38 -4
  185. package/package.json +18 -6
  186. package/resources/review-presets.json +2 -2
  187. package/resources/rv-model-priorities.json +18 -9
  188. package/skills/pi-review/SKILL.md +84 -60
  189. package/skills/pi-review/references/codex-tools.md +18 -11
  190. package/skills/pi-review/references/model-selection.md +49 -28
  191. package/dist/extensions/review.d.ts +0 -2
  192. package/dist/extensions/review.js +0 -22
  193. package/dist/extensions/rv-prompts.d.ts +0 -19
  194. package/dist/extensions/rv-prompts.js +0 -132
@@ -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,19 +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";
21
+ import { registerPanelReviewTool } from "./panel-tool.js";
13
22
 
14
23
  /**
15
24
  * Captured at `session_start` so the synchronous-ish `getArgumentCompletions`
16
25
  * callback (which receives only the argument prefix) can read the live model
17
- * registry without ctx. Stays undefined in non-TUI/print mode; completions then
18
- * gracefully degrade to the static `RV_COMPLETIONS` fallback.
26
+ * registry without ctx. Stays undefined in non-TUI/print mode; the pure
27
+ * completion builder then provides prefix-safe static suggestions.
19
28
  */
20
29
  let capturedModels: ModelInfo[] | undefined;
21
30
  let capturedPrimaryProvider: string | undefined;
22
- let capturedLocale: ReturnType<typeof detectRvLocale> = "en";
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;
23
36
 
24
37
  function sampleSessionText(
25
38
  sessionManager: { getEntries?: () => Array<{ type?: string; content?: unknown }> },
@@ -67,67 +80,200 @@ function toModelInfo(m: {
67
80
  };
68
81
  }
69
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
+
70
100
  export default function piReviewExtension(pi: ExtensionAPI) {
101
+ registerPanelReviewTool(pi);
71
102
  pi.on("session_start", (_event, ctx) => {
72
103
  try {
73
- const available = ctx.modelRegistry?.getAvailable?.() ?? [];
74
- capturedModels = available.map((m) => toModelInfo(m as unknown as Parameters<typeof toModelInfo>[0]));
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;
75
108
  capturedPrimaryProvider = ctx.model?.provider;
109
+ capturedSessionManager = ctx.sessionManager;
76
110
  capturedLocale = detectRvLocale(sampleSessionText(ctx.sessionManager));
111
+ refreshCapturedModels(ctx.modelRegistry, ctx.model?.provider);
77
112
  } catch {
78
113
  capturedModels = undefined;
79
114
  capturedPrimaryProvider = undefined;
80
- capturedLocale = "en";
115
+ capturedLocale = detectRvLocale([]);
81
116
  }
82
117
  });
83
118
 
84
- function localeForHandler(ctx: { sessionManager?: Parameters<typeof sampleSessionText>[0] }): typeof capturedLocale {
119
+ function localeForHandler(ctx?: { sessionManager?: Parameters<typeof sampleSessionText>[0] }): typeof capturedLocale {
85
120
  try {
86
- return detectRvLocale(sampleSessionText(ctx.sessionManager ?? { getEntries: () => [] }));
121
+ const manager = ctx?.sessionManager ?? capturedSessionManager ?? { getEntries: () => [] };
122
+ capturedLocale = detectRvLocale(sampleSessionText(manager));
123
+ return capturedLocale;
87
124
  } catch {
88
125
  return capturedLocale;
89
126
  }
90
127
  }
91
128
 
92
- pi.registerCommand("rv", {
93
- description:
94
- "Delegate pi-review to the agent. Usage: /rv [--mode plan|challenge] [--model id] [--thinking level] [--keep-session] [--no-stream] @target | /rv --continue <handle> [opts] [text] | models",
95
- getArgumentCompletions: (prefix): ReturnType<typeof buildRvCompletions> => {
96
- // Try dynamic completion first (models + thinking + scene templates).
97
- if (capturedModels && capturedModels.length > 0) {
98
- const dynamic = buildRvCompletions(prefix, {
99
- models: capturedModels,
100
- primaryProvider: capturedPrimaryProvider,
101
- locale: capturedLocale,
102
- });
103
- if (dynamic && dynamic.length) return dynamic;
104
- }
105
- // Graceful fallback to static hint list when registry is unavailable.
106
- const filtered = RV_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
107
- return filtered.length
108
- ? filtered.map(({ value, hint }) => ({ value, label: value, description: hint }))
109
- : null;
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];
110
165
  },
111
- handler: async (rawArgs, ctx) => {
112
- const trimmed = rawArgs.trim();
113
- if (!trimmed) {
114
- ctx.ui.notify("/rv needs a target or `models`. Try: /rv @path or /rv models", "warning");
115
- return;
116
- }
166
+ ): Promise<void> {
167
+ if (strategy === "models") {
168
+ const parsed = parseRvArgs("", "models");
169
+ pi.sendUserMessage(buildRvOrchestrationPrompt(parsed, localeForHandler(ctx)));
170
+ return;
171
+ }
117
172
 
118
- const parsed = parseRvArgs(trimmed);
119
- const validation = validateRvParsed(parsed);
120
- if (!validation.ok) {
121
- ctx.ui.notify(validation.message, "warning");
122
- return;
123
- }
173
+ const interactiveRequested = shouldRunInteractiveWizard(rawArgs, parseRvArgs(stripInteractiveToken(rawArgs), strategy));
174
+ const trimmed = stripInteractiveToken(rawArgs).trim();
175
+ let parsed = parseRvArgs(trimmed, strategy);
124
176
 
125
- if (!parsed.modelsOnly && !parsed.target && !parsed.continueHandle) {
126
- ctx.ui.notify("/rv needs a target, --continue <handle>, or `models`.", "warning");
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
+ }
236
+
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
+ );
127
253
  return;
128
254
  }
255
+ }
129
256
 
130
- pi.sendUserMessage(buildRvOrchestrationPrompt(parsed, localeForHandler(ctx)));
131
- },
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),
132
272
  });
133
- }
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),
278
+ });
279
+ }
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
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: "anthropic", id: "claude-opus-4-6", reasoning: true, contextWindow: 200_000, thinkingLevels: ["off", "low", "medium", "high", "xhigh"] }),
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 preset code models first when registered", () => {
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, "gpt-5.5");
93
+ assert.equal(ranked[0].id, "claude-sonnet-4-5");
87
94
  });
88
95
 
89
- it("puts preset plan models first for plan mode", () => {
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, "claude-opus-4-6");
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.5:", deps);
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.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("offers scene templates + models keyword at empty top level", () => {
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.label === ui.listModels));
175
- assert.ok(items!.some((i) => i.label === ui.codePreset));
176
- const tmpl = items!.find((i) => i.label === ui.codePreset)!;
177
- assert.ok(tmpl.value.endsWith(" @"));
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
- const nonReasoning = [{ ...MODELS[2], thinkingLevels: [] }] as ModelInfo[];
222
- const items = buildRvCompletions("", { models: nonReasoning, priorities: DEFAULT_REVIEW_MODEL_PRIORITIES });
223
- assert.ok(items);
224
- assert.ok(!items!.some((i) => i.label === rvUi("en").codePreset));
225
- assert.ok(items!.some((i) => i.label === rvUi("en").listModels));
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
  });