@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.
Files changed (181) hide show
  1. package/README.md +37 -8
  2. package/README.zh-CN.md +31 -5
  3. package/dist/args.d.ts +4 -0
  4. package/dist/args.d.ts.map +1 -1
  5. package/dist/args.js +61 -4
  6. package/dist/args.js.map +1 -1
  7. package/dist/args.test.js +62 -1
  8. package/dist/args.test.js.map +1 -1
  9. package/dist/child-process.d.ts +3 -0
  10. package/dist/child-process.d.ts.map +1 -1
  11. package/dist/child-process.js +27 -5
  12. package/dist/child-process.js.map +1 -1
  13. package/dist/child-process.test.js +42 -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 +7 -5
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -4
  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 +6 -0
  29. package/dist/json-events.d.ts.map +1 -1
  30. package/dist/json-events.js +48 -6
  31. package/dist/json-events.js.map +1 -1
  32. package/dist/json-events.test.js +20 -1
  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.js +33 -2
  51. package/dist/panel-cancellation.test.js.map +1 -1
  52. package/dist/panel-cli-ui.test.d.ts +2 -0
  53. package/dist/panel-cli-ui.test.d.ts.map +1 -0
  54. package/dist/panel-cli-ui.test.js +91 -0
  55. package/dist/panel-cli-ui.test.js.map +1 -0
  56. package/dist/panel-cli.test.js +37 -39
  57. package/dist/panel-cli.test.js.map +1 -1
  58. package/dist/panel-config.d.ts +38 -0
  59. package/dist/panel-config.d.ts.map +1 -1
  60. package/dist/panel-config.js +135 -2
  61. package/dist/panel-config.js.map +1 -1
  62. package/dist/panel-config.test.js +79 -2
  63. package/dist/panel-config.test.js.map +1 -1
  64. package/dist/panel-loop.test.js +1 -1
  65. package/dist/panel-loop.test.js.map +1 -1
  66. package/dist/panel-tools.test.js +1 -1
  67. package/dist/panel-tools.test.js.map +1 -1
  68. package/dist/panel-ui.d.ts +20 -0
  69. package/dist/panel-ui.d.ts.map +1 -0
  70. package/dist/panel-ui.js +84 -0
  71. package/dist/panel-ui.js.map +1 -0
  72. package/dist/panel-ui.test.d.ts +2 -0
  73. package/dist/panel-ui.test.d.ts.map +1 -0
  74. package/dist/panel-ui.test.js +122 -0
  75. package/dist/panel-ui.test.js.map +1 -0
  76. package/dist/panel-view.d.ts +1 -0
  77. package/dist/panel-view.d.ts.map +1 -1
  78. package/dist/panel-view.js +40 -6
  79. package/dist/panel-view.js.map +1 -1
  80. package/dist/panel-view.test.js +68 -1
  81. package/dist/panel-view.test.js.map +1 -1
  82. package/dist/panel.d.ts +11 -1
  83. package/dist/panel.d.ts.map +1 -1
  84. package/dist/panel.js +67 -21
  85. package/dist/panel.js.map +1 -1
  86. package/dist/prompt.d.ts +2 -0
  87. package/dist/prompt.d.ts.map +1 -1
  88. package/dist/prompt.js +34 -3
  89. package/dist/prompt.js.map +1 -1
  90. package/dist/prompt.test.js +21 -2
  91. package/dist/prompt.test.js.map +1 -1
  92. package/dist/review-events.test.js +1 -1
  93. package/dist/review-events.test.js.map +1 -1
  94. package/dist/review-result.test.js +1 -1
  95. package/dist/review-result.test.js.map +1 -1
  96. package/dist/review.js +3 -3
  97. package/dist/review.js.map +1 -1
  98. package/dist/review.meta.test.js +1 -1
  99. package/dist/review.meta.test.js.map +1 -1
  100. package/dist/skill-protocol.test.js +1 -1
  101. package/dist/skill-protocol.test.js.map +1 -1
  102. package/dist/skill-update.test.d.ts +2 -0
  103. package/dist/skill-update.test.d.ts.map +1 -0
  104. package/dist/skill-update.test.js +23 -0
  105. package/dist/skill-update.test.js.map +1 -0
  106. package/dist/skill.d.ts +22 -0
  107. package/dist/skill.d.ts.map +1 -1
  108. package/dist/skill.js +109 -22
  109. package/dist/skill.js.map +1 -1
  110. package/dist/types.d.ts +23 -1
  111. package/dist/types.d.ts.map +1 -1
  112. package/dist/types.js.map +1 -1
  113. package/dist/ui-client.d.ts +13 -0
  114. package/dist/ui-client.d.ts.map +1 -0
  115. package/dist/ui-client.js +161 -0
  116. package/dist/ui-client.js.map +1 -0
  117. package/dist/ui-client.test.d.ts +2 -0
  118. package/dist/ui-client.test.d.ts.map +1 -0
  119. package/dist/ui-client.test.js +48 -0
  120. package/dist/ui-client.test.js.map +1 -0
  121. package/dist/ui-html.d.ts +7 -0
  122. package/dist/ui-html.d.ts.map +1 -0
  123. package/dist/ui-html.js +52 -0
  124. package/dist/ui-html.js.map +1 -0
  125. package/dist/ui-html.test.d.ts +2 -0
  126. package/dist/ui-html.test.d.ts.map +1 -0
  127. package/dist/ui-html.test.js +26 -0
  128. package/dist/ui-html.test.js.map +1 -0
  129. package/dist/ui-server-main.d.ts +3 -0
  130. package/dist/ui-server-main.d.ts.map +1 -0
  131. package/dist/ui-server-main.js +128 -0
  132. package/dist/ui-server-main.js.map +1 -0
  133. package/dist/ui-server-main.test.d.ts +2 -0
  134. package/dist/ui-server-main.test.d.ts.map +1 -0
  135. package/dist/ui-server-main.test.js +123 -0
  136. package/dist/ui-server-main.test.js.map +1 -0
  137. package/dist/ui-server.d.ts +42 -0
  138. package/dist/ui-server.d.ts.map +1 -0
  139. package/dist/ui-server.js +233 -0
  140. package/dist/ui-server.js.map +1 -0
  141. package/dist/ui-server.test.d.ts +2 -0
  142. package/dist/ui-server.test.d.ts.map +1 -0
  143. package/dist/ui-server.test.js +236 -0
  144. package/dist/ui-server.test.js.map +1 -0
  145. package/dist/update.d.ts +2 -0
  146. package/dist/update.d.ts.map +1 -1
  147. package/dist/update.js +80 -20
  148. package/dist/update.js.map +1 -1
  149. package/dist/update.test.d.ts +2 -0
  150. package/dist/update.test.d.ts.map +1 -0
  151. package/dist/update.test.js +17 -0
  152. package/dist/update.test.js.map +1 -0
  153. package/extensions/panel-tool.test.ts +366 -8
  154. package/extensions/panel-tool.ts +289 -68
  155. package/extensions/review.test.ts +79 -0
  156. package/extensions/review.ts +188 -44
  157. package/extensions/rv-completions.test.ts +177 -18
  158. package/extensions/rv-completions.ts +490 -60
  159. package/extensions/rv-interactive.test.ts +562 -0
  160. package/extensions/rv-interactive.ts +616 -0
  161. package/extensions/rv-locale.test.ts +12 -4
  162. package/extensions/rv-locale.ts +44 -11
  163. package/extensions/rv-model-picker.test.ts +172 -0
  164. package/extensions/rv-model-picker.ts +313 -0
  165. package/extensions/rv-model-priorities.test.ts +29 -9
  166. package/extensions/rv-model-priorities.ts +23 -8
  167. package/extensions/rv-prompts.test.ts +274 -2
  168. package/extensions/rv-prompts.ts +479 -33
  169. package/extensions/rv-resolve.test.ts +189 -0
  170. package/extensions/rv-resolve.ts +313 -0
  171. package/extensions/rv-semantic.test.ts +1 -1
  172. package/extensions/rv-semantic.ts +38 -4
  173. package/package.json +7 -4
  174. package/resources/rv-model-priorities.json +18 -9
  175. package/skills/pi-review/SKILL.md +84 -60
  176. package/skills/pi-review/references/codex-tools.md +18 -11
  177. package/skills/pi-review/references/model-selection.md +49 -28
  178. package/dist/extensions/review.d.ts +0 -2
  179. package/dist/extensions/review.js +0 -22
  180. package/dist/extensions/rv-prompts.d.ts +0 -19
  181. package/dist/extensions/rv-prompts.js +0 -132
@@ -39,6 +39,15 @@ export const RV_FLAGS = [
39
39
  "--keep-session",
40
40
  "--no-stream",
41
41
  "--continue",
42
+ "--max-rounds",
43
+ "--until",
44
+ "--reviewers",
45
+ "--panel",
46
+ "--reviewer-model",
47
+ "--consensus",
48
+ "--min-agree",
49
+ "--consensus-model",
50
+ "--concurrency",
42
51
  ] as const;
43
52
 
44
53
  export const MODE_HINTS: Record<string, string> = {
@@ -47,15 +56,65 @@ export const MODE_HINTS: Record<string, string> = {
47
56
  challenge: "Adversarial plan review",
48
57
  };
49
58
 
59
+ export const CONSENSUS_HINTS: Record<string, string> = {
60
+ any: "Any single actionable reviewer confirms",
61
+ quorum: "At least --min-agree reviewers (default 2)",
62
+ majority: "floor(n/2)+1 reviewers",
63
+ unanimous: "All reviewers must agree",
64
+ };
65
+
66
+ export const PANEL_PRESET_HINTS: Record<string, string> = {
67
+ "code-experts": "correctness + security + testing (quorum 2)",
68
+ };
69
+
50
70
  export const FLAG_HINTS: Record<string, string> = {
51
71
  "--mode": "Review mode: code | plan | challenge",
52
- "--model": "provider/model[:thinking]",
72
+ "--model": "provider/model[:thinking] (short ids ok)",
53
73
  "--thinking": "off | minimal | low | medium | high | xhigh",
54
74
  "--keep-session": "Keep session for /rv --continue follow-up",
55
75
  "--no-stream": "Buffer output until exit (not default)",
56
76
  "--continue": "Resume an existing review session",
77
+ "--max-rounds": "Loop hard budget: how many review gates (not reviewer count)",
78
+ "--until": "Loop stop goal: clean",
79
+ "--reviewers": "Panel width: independent reviewers per gate (1-8)",
80
+ "--panel": "Named expert panel preset (cannot combine with --reviewers)",
81
+ "--reviewer-model": "Per-reviewer model: pick rK= then model (repeatable)",
82
+ "--consensus": "any | quorum | majority | unanimous",
83
+ "--min-agree": "Quorum threshold (quorum only)",
84
+ "--consensus-model": "Model for semantic adjudication only",
85
+ "--concurrency": "Bound parallel reviewers (≤ reviewers)",
57
86
  };
58
87
 
88
+ /** Infer reviewer ids available for --reviewer-model secondary menu. */
89
+ export function reviewerIdsForHead(head: string[]): string[] {
90
+ const panelIdx = head.indexOf("--panel");
91
+ if (panelIdx >= 0) {
92
+ const name = head[panelIdx + 1];
93
+ if (name === "code-experts") return ["correctness", "security", "testing"];
94
+ // Unknown preset: still offer generic slots as a fallback.
95
+ }
96
+ const reviewersIdx = head.indexOf("--reviewers");
97
+ if (reviewersIdx >= 0) {
98
+ const n = Number(head[reviewersIdx + 1]);
99
+ if (Number.isSafeInteger(n) && n >= 1 && n <= 8) {
100
+ return Array.from({ length: n }, (_, i) => `r${i + 1}`);
101
+ }
102
+ }
103
+ // Default panel path (/rv without width): code-experts style ids.
104
+ return ["correctness", "security", "testing"];
105
+ }
106
+
107
+ function assignedReviewerIds(head: string[]): Set<string> {
108
+ const assigned = new Set<string>();
109
+ for (let i = 0; i < head.length; i++) {
110
+ if (head[i] !== "--reviewer-model") continue;
111
+ const value = head[i + 1] ?? "";
112
+ const eq = value.indexOf("=");
113
+ if (eq > 0) assigned.add(value.slice(0, eq));
114
+ }
115
+ return assigned;
116
+ }
117
+
59
118
  /** Minimal model metadata we need; converted from the host's Model registry. */
60
119
  export interface ModelInfo {
61
120
  provider: string;
@@ -77,11 +136,15 @@ export interface ReviewSignals {
77
136
  primaryProvider?: string;
78
137
  }
79
138
 
139
+ export type CompletionStrategy = "panel" | "loop" | "models";
140
+
80
141
  export interface CompletionDeps {
81
142
  models?: ModelInfo[];
82
143
  primaryProvider?: string;
83
144
  priorities?: ReviewModelPriorities;
84
145
  locale?: RvLocale;
146
+ /** Slash-command strategy so completions stay relevant for /rv, /rv-loop, /rv-models. */
147
+ strategy?: CompletionStrategy;
85
148
  }
86
149
 
87
150
  function localeOf(deps: CompletionDeps): RvLocale {
@@ -290,17 +353,23 @@ function modelItems(
290
353
  .join(" · "),
291
354
  };
292
355
  });
293
- // Contiguous matching for model ids (subsequence is too loose for ids like
294
- // "claude-opus" which would match "ope" via o-p...-e).
295
- const q = tail;
356
+ // Contiguous + separator-insensitive matching for human short names
357
+ // (gpt55 / gpt-5.5 / openai/gpt-5.5). Avoid raw subsequence matching.
358
+ const q = tail.toLowerCase();
359
+ const qKey = q.replace(/[^a-z0-9/]+/g, "");
296
360
  const filtered = items.filter((it) => {
297
- const label = it.label;
361
+ const label = it.label.toLowerCase();
362
+ const labelKey = label.replace(/[^a-z0-9/]+/g, "");
363
+ const id = label.split("/")[1] ?? "";
364
+ const provider = label.split("/")[0] ?? "";
365
+ const idKey = id.replace(/[^a-z0-9]+/g, "");
298
366
  return (
299
367
  label.startsWith(q) ||
300
368
  label.includes(q) ||
301
- // also allow matching by bare id / provider segment
302
- label.split("/")[1]?.startsWith(q) ||
303
- label.split("/")[0]?.startsWith(q)
369
+ id.startsWith(q) ||
370
+ provider.startsWith(q) ||
371
+ labelKey.includes(qKey) ||
372
+ idKey.includes(qKey.replace(/\//g, ""))
304
373
  );
305
374
  });
306
375
  return filtered.length ? filtered : null;
@@ -348,6 +417,14 @@ function thinkingSuffixForModel(m: ModelInfo, want?: string): string {
348
417
  return lvl ? `:${lvl}` : "";
349
418
  }
350
419
 
420
+ /** Presets are opt-in to avoid drowning flag/model completions. */
421
+ export function wantsPresetCompletions(tail: string): boolean {
422
+ const q = tail.trim().toLowerCase();
423
+ if (!q) return false;
424
+ return /^(preset|模板|预设|推荐|code|plan|front|challenge|代码|方案|前端|对抗|loop|关单)/i.test(q)
425
+ || /preset|模板|预设|推荐|code review|plan review|frontend|challenge|代码审核|方案审核|前端|对抗|关单/i.test(q);
426
+ }
427
+
351
428
  function sceneTemplates(
352
429
  models: ModelInfo[],
353
430
  sig: ReviewSignals,
@@ -355,91 +432,319 @@ function sceneTemplates(
355
432
  tail: string,
356
433
  deps: CompletionDeps,
357
434
  ): AutocompleteItem[] {
435
+ // Empty top-level should stay quiet: flags + model typing first.
436
+ if (!wantsPresetCompletions(tail)) return [];
437
+
358
438
  const priorities = getPriorities(deps);
359
439
  const codePrimary = primaryPresetForProfile(models, "code", priorities);
360
440
  const planPrimary = primaryPresetForProfile(models, "plan", priorities);
361
441
  const frontendPrimary = primaryPresetForProfile(models, "frontend", priorities);
362
-
442
+ const strategy = deps.strategy ?? "panel";
363
443
  const ui = rvUi(localeOf(deps));
364
- const templates: { cmd: string; label: string }[] = [];
365
- if (codePrimary) {
366
- const m = codePrimary.model;
367
- templates.push({
368
- cmd: `${ui.modeCode} --model ${m.label}${thinkingSuffixForModel(m, codePrimary.thinking ?? "xhigh")} @`,
369
- label: ui.codePreset,
370
- });
371
- }
372
- if (frontendPrimary) {
373
- const m = frontendPrimary.model;
374
- templates.push({
375
- cmd: `${ui.modeCode} --model ${m.label}${thinkingSuffixForModel(m, frontendPrimary.thinking)} @`,
376
- label: ui.frontendPreset,
377
- });
444
+ const templates: { cmd: string; label: string; description?: string }[] = [];
445
+
446
+ if (strategy === "models") {
447
+ return [{
448
+ value: `${headPrefix}${tail}`,
449
+ label: ui.listModels,
450
+ description: ui.listModelsDesc,
451
+ }].filter((t) => !tail || fuzzyMatch(t.label, tail) || fuzzyMatch("models", tail));
378
452
  }
379
- if (planPrimary) {
380
- const m = planPrimary.model;
381
- templates.push({
382
- cmd: `${ui.modePlan} --model ${m.label}${thinkingSuffixForModel(m, planPrimary.thinking ?? "high")} @`,
383
- label: ui.planPreset,
384
- });
385
- templates.push({
386
- cmd: `${ui.modeChallenge} ${ui.keepSession} --model ${m.label}${thinkingSuffixForModel(m, "xhigh")} @`,
387
- label: ui.challengePreset,
388
- });
453
+
454
+ if (strategy === "loop") {
455
+ if (codePrimary) {
456
+ const m = codePrimary.model;
457
+ const cmd1 = `--model ${m.label}${thinkingSuffixForModel(m, codePrimary.thinking ?? "high")} @src`;
458
+ templates.push({
459
+ cmd: cmd1,
460
+ label: ui.loopCodePreset,
461
+ description: `${ui.loopCodeDesc} · ${cmd1}`,
462
+ });
463
+ const cmd2 = `--max-rounds 2 --model ${m.label}${thinkingSuffixForModel(m, codePrimary.thinking ?? "high")} fix until clean @src`;
464
+ templates.push({
465
+ cmd: cmd2,
466
+ label: ui.loopTwoRounds,
467
+ description: cmd2,
468
+ });
469
+ }
470
+ if (planPrimary) {
471
+ const m = planPrimary.model;
472
+ const cmd = `--mode plan --model ${m.label}${thinkingSuffixForModel(m, planPrimary.thinking ?? "high")} @docs`;
473
+ templates.push({
474
+ cmd,
475
+ label: ui.loopPlanPreset,
476
+ description: cmd,
477
+ });
478
+ }
479
+ } else {
480
+ if (codePrimary) {
481
+ const m = codePrimary.model;
482
+ const cmd = `--model ${m.label}${thinkingSuffixForModel(m, codePrimary.thinking ?? "xhigh")} @src`;
483
+ templates.push({ cmd, label: ui.codePreset, description: cmd });
484
+ }
485
+ if (frontendPrimary) {
486
+ const m = frontendPrimary.model;
487
+ const cmd = `--model ${m.label}${thinkingSuffixForModel(m, frontendPrimary.thinking)} @src`;
488
+ templates.push({ cmd, label: ui.frontendPreset, description: cmd });
489
+ }
490
+ if (planPrimary) {
491
+ const m = planPrimary.model;
492
+ const planCmd = `--mode plan --model ${m.label}${thinkingSuffixForModel(m, planPrimary.thinking ?? "high")} @docs`;
493
+ templates.push({ cmd: planCmd, label: ui.planPreset, description: planCmd });
494
+ const challengeCmd = `--mode challenge --keep-session --model ${m.label}${thinkingSuffixForModel(m, "xhigh")} @docs`;
495
+ templates.push({ cmd: challengeCmd, label: ui.challengePreset, description: challengeCmd });
496
+ }
389
497
  }
390
498
 
391
499
  return templates
392
- .filter((t) => fuzzyMatch(t.label, tail) || t.cmd.includes(tail))
500
+ .filter((t) => {
501
+ const q = tail.trim().toLowerCase();
502
+ if (!q || q === "preset" || q === "预设" || q === "模板" || q === "推荐") return true;
503
+ return fuzzyMatch(t.label, tail) || t.cmd.toLowerCase().includes(q) || fuzzyMatch(t.description ?? "", tail);
504
+ })
393
505
  .map((t) => ({
394
506
  value: `${headPrefix}${t.cmd}`,
395
507
  label: t.label,
396
- description: t.cmd,
508
+ // Put the full command in description so the TUI shows what will be inserted.
509
+ description: t.description ?? t.cmd,
397
510
  }));
398
511
  }
399
512
 
400
- function flagItems(head: string[], tail: string, headPrefix: string): AutocompleteItem[] | null {
513
+ function flagItems(
514
+ head: string[],
515
+ tail: string,
516
+ headPrefix: string,
517
+ strategy: CompletionStrategy = "panel",
518
+ ): AutocompleteItem[] | null {
401
519
  // Don't re-suggest boolean flags already present.
402
520
  const present = new Set(head);
403
- const candidates = filterList(RV_FLAGS, tail).filter((f) => {
521
+ const hasReviewers = head.includes("--reviewers");
522
+ const hasPanel = head.includes("--panel");
523
+ const allowed = RV_FLAGS.filter((f) => {
524
+ if (strategy === "models") return false;
525
+ if (strategy === "loop") {
526
+ // Loop rejects keep-session / continue; max-rounds is loop-only.
527
+ if (f === "--keep-session" || f === "--continue") return false;
528
+ return true;
529
+ }
530
+ // Panel strategy (/rv): no loop-only flags (single gate).
531
+ if (f === "--max-rounds" || f === "--until") return false;
532
+ return true;
533
+ }).filter((f) => {
534
+ // Mutually exclusive panel selectors.
535
+ if (f === "--reviewers" && hasPanel) return false;
536
+ if (f === "--panel" && hasReviewers) return false;
537
+ return true;
538
+ });
539
+ const candidates = filterList(allowed, tail).filter((f) => {
404
540
  if (f === "--keep-session" || f === "--no-stream") return !present.has(f);
541
+ // Value flags: hide if already present once.
542
+ if ([
543
+ "--mode", "--model", "--thinking", "--continue", "--max-rounds", "--until",
544
+ "--reviewers", "--panel", "--consensus", "--min-agree", "--consensus-model", "--concurrency",
545
+ ].includes(f) && present.has(f)) return false;
546
+ // --reviewer-model is repeatable; keep offering until every reviewer id is assigned.
547
+ if (f === "--reviewer-model") {
548
+ const ids = reviewerIdsForHead(head);
549
+ const assigned = assignedReviewerIds(head);
550
+ if (ids.length > 0 && ids.every((id) => assigned.has(id))) return false;
551
+ }
405
552
  return true;
406
553
  });
407
554
  return wrap(candidates, headPrefix, FLAG_HINTS);
408
555
  }
409
556
 
557
+ function reviewerModelCompletions(
558
+ head: string[],
559
+ tail: string,
560
+ headPrefix: string,
561
+ deps: CompletionDeps,
562
+ sig: ReviewSignals,
563
+ ): AutocompleteItem[] | null {
564
+ const ids = reviewerIdsForHead(head);
565
+ const assigned = assignedReviewerIds(head);
566
+ const eq = tail.indexOf("=");
567
+
568
+ // Secondary menu stage 1: pick reviewer id → inserts `id=`
569
+ if (eq === -1) {
570
+ const open = ids.filter((id) => !assigned.has(id) || tail.startsWith(id));
571
+ const filtered = filterList(open.length ? open : ids, tail);
572
+ if (!filtered.length) return null;
573
+ return filtered.map((id) => ({
574
+ value: `${headPrefix}${id}=`,
575
+ label: `${id}=`,
576
+ description: localeOf(deps) === "zh"
577
+ ? `为 ${id} 选择模型(下一级)`
578
+ : `Choose model for ${id} (next step)`,
579
+ }));
580
+ }
581
+
582
+ // Secondary menu stage 2: after `id=`, offer catalog models.
583
+ const reviewerId = tail.slice(0, eq);
584
+ const modelQuery = tail.slice(eq + 1);
585
+ const models = deps.models ?? [];
586
+ if (!models.length) {
587
+ // Still help shape the token when registry is cold.
588
+ if (!modelQuery) {
589
+ return [{
590
+ value: `${headPrefix}${reviewerId}=`,
591
+ label: `${reviewerId}=<model>`,
592
+ description: localeOf(deps) === "zh" ? "输入 provider/model 或短名" : "Type provider/model or short id",
593
+ }];
594
+ }
595
+ return [{
596
+ value: `${headPrefix}${reviewerId}=${modelQuery}`,
597
+ label: `${reviewerId}=${modelQuery}`,
598
+ description: localeOf(deps) === "zh" ? "使用输入的模型 token" : "Use typed model token",
599
+ }];
600
+ }
601
+ const ranked = modelItems(models, sig, "", modelQuery, deps) ?? [];
602
+ if (!ranked.length) return null;
603
+ return ranked.map((item) => ({
604
+ value: `${headPrefix}${reviewerId}=${item.label}`,
605
+ label: `${reviewerId}=${item.label}`,
606
+ description: item.description,
607
+ }));
608
+ }
609
+
610
+ function looksLikeModelQuery(tail: string, models: ModelInfo[] = []): boolean {
611
+ if (!tail || tail.startsWith("-") || tail.startsWith("@")) return false;
612
+ // provider/model, bare id fragments, or model:thinking
613
+ if (tail.includes("/") || tail.includes(":")) return true;
614
+ if (/^(gpt|claude|kimi|glm|opus|sonnet|deepseek|minimax|o[0-9]|gemini|openai|anthropic|codex|zenmux|moonshot|px)/i.test(tail)) {
615
+ return true;
616
+ }
617
+ // Match against live catalog providers / ids so short provider names complete.
618
+ const q = tail.toLowerCase();
619
+ if (q.length >= 2 && models.some((m) => m.provider.toLowerCase().includes(q) || m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q))) {
620
+ return true;
621
+ }
622
+ // short alphanumeric model-ish tokens (gpt55, kimi-k2)
623
+ return /^[a-z0-9][a-z0-9._-]{1,}$/i.test(tail) && /[0-9]|gpt|claude|kimi|glm|opus|sonnet/i.test(tail);
624
+ }
625
+
410
626
  function topLevelCompletions(
411
627
  head: string[],
628
+ rawHead: string[],
412
629
  tail: string,
413
630
  deps: CompletionDeps,
414
631
  headPrefix: string,
415
632
  sig: ReviewSignals,
416
633
  ): AutocompleteItem[] | null {
417
634
  const items: AutocompleteItem[] = [];
635
+ const strategy = deps.strategy ?? "panel";
636
+ const ui = rvUi(localeOf(deps));
637
+
638
+ if (strategy === "models") {
639
+ // /rv-models takes no target; only remind the user.
640
+ if (!tail || fuzzyMatch(ui.listModels, tail) || fuzzyMatch("models", tail)) {
641
+ items.push({ value: `${headPrefix}${tail}`, label: ui.listModels, description: ui.listModelsDesc });
642
+ }
643
+ return items.length ? items : null;
644
+ }
645
+
646
+ // After completing options, make the target boundary explicit. This lets a
647
+ // Tab-first flow safely append natural language without a value-taking flag
648
+ // accidentally consuming it.
649
+ if (!tail && !head.includes("--") && head.some((token) => token.startsWith("--"))) {
650
+ const zh = localeOf(deps) === "zh";
651
+ items.push({
652
+ value: `${headPrefix}-- `,
653
+ label: zh ? "开始输入审查目标" : "Start review target",
654
+ description: zh
655
+ ? "参数已结束,接下来输入自然语言或 @路径"
656
+ : "Finish options, then type natural language or @path",
657
+ });
658
+ }
659
+
660
+ // Quiet empty top-level: interactive wizard first, then a few flags.
661
+ if (!tail) {
662
+ const zh = localeOf(deps) === "zh";
663
+ items.push({
664
+ value: `${headPrefix}-i`,
665
+ label: zh ? "-i · 交互向导(推荐)" : "-i · interactive wizard (recommended)",
666
+ description: zh
667
+ ? "对话框选择人数、模型、思考强度 — 不用 Tab"
668
+ : "Dialog picker for reviewers, models, thinking — no tab",
669
+ });
670
+ const flags = flagItems(head, "--", headPrefix, strategy) ?? [];
671
+ items.push(...flags.slice(0, 5));
672
+ items.push({
673
+ value: headPrefix,
674
+ label: ui.presetHint,
675
+ description: localeOf(deps) === "zh" ? "需要模板时再输入「预设」" : "Type preset when you want templates",
676
+ });
677
+ return items.length ? items : null;
678
+ }
418
679
 
419
- // Scene templates (only at top level, before any target/mode chosen is fine).
680
+ // Opt-in scene templates only.
420
681
  if (deps.models?.length) {
421
682
  items.push(...sceneTemplates(deps.models, sig, headPrefix, tail, deps));
422
683
  }
423
684
 
424
- items.push(...semanticCompletionItems(localeOf(deps), tail, headPrefix, head));
685
+ // Panel keeps semantic phrases when the user is typing mode-ish text.
686
+ if (strategy === "panel" && !looksLikeModelQuery(tail, deps.models ?? [])) {
687
+ items.push(...semanticCompletionItems(localeOf(deps), tail, headPrefix, head, rawHead));
688
+ const hasListModelsItem = items.some((i) => i.label === ui.listModels);
689
+ if (
690
+ !hasListModelsItem &&
691
+ (fuzzyMatch(ui.modelsKeyword, tail) ||
692
+ ui.modelsKeyword.startsWith(tail) ||
693
+ fuzzyMatch("models", tail) ||
694
+ fuzzyMatch(ui.listModels, tail))
695
+ ) {
696
+ items.push({
697
+ value: `${headPrefix}${ui.listModels}`,
698
+ label: ui.listModels,
699
+ description: ui.listModelsDesc,
700
+ });
701
+ }
702
+ }
425
703
 
426
- const ui = rvUi(localeOf(deps));
427
- const hasListModelsItem = items.some((i) => i.label === ui.listModels);
428
- if (
429
- !hasListModelsItem &&
430
- (fuzzyMatch(ui.modelsKeyword, tail) ||
431
- ui.modelsKeyword.startsWith(tail) ||
432
- fuzzyMatch("models", tail))
433
- ) {
434
- items.push({
435
- value: `${headPrefix}${ui.listModels}`,
436
- label: ui.listModels,
437
- description: ui.listModelsDesc,
704
+ // Bare model typing: `/rv-loop gpt-5.5` / `/rv openai-codex/gpt-5.5`
705
+ // Also support `/rv openai-codex/gpt-5.5:` thinking suffixes without requiring --model first.
706
+ if (deps.models?.length && looksLikeModelQuery(tail, deps.models)) {
707
+ const colonIdx = tail.lastIndexOf(":");
708
+ if (colonIdx !== -1 && tail.slice(0, colonIdx).includes("/")) {
709
+ const thinking = thinkingSuffixItems(tail, deps.models, `${headPrefix}--model `, sig, deps);
710
+ if (thinking?.length) items.unshift(...thinking);
711
+ } else {
712
+ const modelMatches = modelItems(deps.models, sig, `${headPrefix}--model `, tail, deps);
713
+ if (modelMatches?.length) {
714
+ // Prefer inserting as --model <id> so the parser/resolver gets a clean flag.
715
+ items.unshift(
716
+ ...modelMatches.map((item) => ({
717
+ ...item,
718
+ description: [item.description, strategy === "loop" ? "loop model" : "panel model"].filter(Boolean).join(" · "),
719
+ })),
720
+ );
721
+ // If the user already typed an exact catalog id, also offer thinking suffixes.
722
+ const exact = deps.models.find((m) => m.label === tail || m.id === tail);
723
+ if (exact?.thinkingLevels?.length) {
724
+ for (const level of exact.thinkingLevels) {
725
+ items.unshift({
726
+ value: `${headPrefix}--model ${exact.label}:${level}`,
727
+ label: `${exact.label}:${level}`,
728
+ description: thinkingDescription(level, sig, localeOf(deps)),
729
+ });
730
+ }
731
+ }
732
+ }
733
+ }
734
+ } else if (!deps.models?.length && looksLikeModelQuery(tail)) {
735
+ // Registry-cold fallback: preserve every already-entered argument while
736
+ // wrapping the typed token as --model. Pi replaces the whole arg string.
737
+ items.unshift({
738
+ value: `${headPrefix}--model ${tail}`,
739
+ label: `--model ${tail}`,
740
+ description: localeOf(deps) === "zh"
741
+ ? "模型目录尚未加载;保留现有目标并在执行时解析"
742
+ : "Catalog not loaded; preserve target and resolve at execution",
438
743
  });
439
744
  }
440
745
 
441
746
  if (tail.startsWith("--")) {
442
- const flags = flagItems(head, tail, headPrefix);
747
+ const flags = flagItems(head, tail, headPrefix, strategy);
443
748
  if (flags) items.push(...flags);
444
749
  }
445
750
 
@@ -450,6 +755,37 @@ function topLevelCompletions(
450
755
  // Public entry point
451
756
  // ---------------------------------------------------------------------------
452
757
 
758
+ function interactiveTriggerCompletions(
759
+ tail: string,
760
+ headPrefix: string,
761
+ strategy: CompletionStrategy,
762
+ locale: RvLocale,
763
+ ): AutocompleteItem[] | null {
764
+ const q = tail.trim().toLowerCase();
765
+ // Only intercept pure interactive triggers, not unrelated flags like --input.
766
+ if (!q || !(/^(-i|--i|--in|--int|--inter|--interactive|interactive|i)$/i.test(q) || q === "-")) {
767
+ // Allow "-i" / "--interactive" / "interactive" prefixes while typing.
768
+ if (!/^(interactive|--interactive|-i)/i.test(q) && q !== "-") return null;
769
+ }
770
+ if (q.startsWith("--") && !q.startsWith("--i") && !q.startsWith("--interactive")) return null;
771
+ if (q.startsWith("-") && q !== "-" && !q.startsWith("-i") && !q.startsWith("--")) return null;
772
+
773
+ const zh = locale === "zh";
774
+ const label = zh ? "交互向导(对话框选模型)" : "Interactive wizard (dialog model picker)";
775
+ const description = strategy === "loop"
776
+ ? (zh ? "方向键选择 reviewer / 模型,无需 Tab 补全" : "Arrow-key pick reviewers & models; no tab completion")
777
+ : (zh ? "方向键配置 panel 审查" : "Arrow-key configure panel review");
778
+ return [{
779
+ value: `${headPrefix}-i`,
780
+ label: `-i · ${label}`,
781
+ description,
782
+ }, {
783
+ value: `${headPrefix}--interactive`,
784
+ label: `--interactive · ${label}`,
785
+ description,
786
+ }];
787
+ }
788
+
453
789
  export function buildRvCompletions(
454
790
  prefix: string,
455
791
  deps: CompletionDeps,
@@ -458,15 +794,32 @@ export function buildRvCompletions(
458
794
  const joinedHead = rawHead.join(" ");
459
795
  const headPrefix = joinedHead ? `${joinedHead} ` : "";
460
796
  const sig = extractSignals(head, deps);
797
+ const strategy = deps.strategy ?? "panel";
798
+
799
+ // 0) Interactive wizard triggers — do not fall through to the flag soup.
800
+ if (!prev && strategy !== "models") {
801
+ const interactive = interactiveTriggerCompletions(tail, headPrefix, strategy, localeOf(deps));
802
+ if (interactive) return interactive;
803
+ }
461
804
 
462
805
  // 1) --model value position (model list or :thinking suffix).
463
806
  if (prev === "--model") {
807
+ const models = deps.models ?? [];
808
+ if (models.length === 0) {
809
+ return [{
810
+ value: `${headPrefix}${tail}`,
811
+ label: tail || "<provider/model>",
812
+ description: localeOf(deps) === "zh"
813
+ ? "模型目录尚未加载;继续输入 provider/model 或短名"
814
+ : "Catalog not loaded; type provider/model or short id",
815
+ }];
816
+ }
464
817
  const colonIdx = tail.lastIndexOf(":");
465
818
  if (colonIdx !== -1 && tail.slice(0, colonIdx).includes("/")) {
466
- const items = thinkingSuffixItems(tail, deps.models ?? [], headPrefix, sig, deps);
819
+ const items = thinkingSuffixItems(tail, models, headPrefix, sig, deps);
467
820
  return items;
468
821
  }
469
- return modelItems(deps.models ?? [], sig, headPrefix, tail, deps);
822
+ return modelItems(models, sig, headPrefix, tail, deps);
470
823
  }
471
824
 
472
825
  // 2) --mode value position.
@@ -479,17 +832,94 @@ export function buildRvCompletions(
479
832
  return wrap(filterList(THINKING_LEVELS, tail), headPrefix);
480
833
  }
481
834
 
482
- // 4) --continue value position: defer (no static list; handler owns it).
835
+ // 4) --max-rounds value position (loop only).
836
+ if (prev === "--max-rounds") {
837
+ if (strategy !== "loop") return null;
838
+ const rounds = ["1", "2", "3"];
839
+ return wrap(filterList(rounds, tail), headPrefix, {
840
+ "1": "One gate, then host fix point",
841
+ "2": "Two review gates",
842
+ "3": "Three review gates",
843
+ });
844
+ }
845
+
846
+ // 4b) --until value position (loop only).
847
+ if (prev === "--until") {
848
+ if (strategy !== "loop") return null;
849
+ return wrap(filterList(["clean"], tail), headPrefix, {
850
+ clean: "Stop only when the clean gate is met (hard-capped by --max-rounds)",
851
+ });
852
+ }
853
+
854
+ // 5) --reviewers value position (panel width per gate).
855
+ if (prev === "--reviewers") {
856
+ const counts = ["2", "3", "4", "5"];
857
+ return wrap(filterList(counts, tail), headPrefix, {
858
+ "2": "Two independent reviewers · then set --reviewer-model r1=…",
859
+ "3": "Three independent reviewers · then set --reviewer-model r1=…",
860
+ "4": "Four independent reviewers",
861
+ "5": "Five independent reviewers",
862
+ });
863
+ }
864
+
865
+ // 5b) --reviewer-model cascading menu: id= then model list.
866
+ if (prev === "--reviewer-model") {
867
+ return reviewerModelCompletions(head, tail, headPrefix, deps, sig);
868
+ }
869
+
870
+ // 6) --panel value position.
871
+ if (prev === "--panel") {
872
+ return wrap(filterList(Object.keys(PANEL_PRESET_HINTS), tail), headPrefix, PANEL_PRESET_HINTS);
873
+ }
874
+
875
+ // 7) --consensus value position.
876
+ if (prev === "--consensus") {
877
+ return wrap(filterList(Object.keys(CONSENSUS_HINTS), tail), headPrefix, CONSENSUS_HINTS);
878
+ }
879
+
880
+ // 8) --min-agree value position.
881
+ if (prev === "--min-agree") {
882
+ return wrap(filterList(["2", "3", "4"], tail), headPrefix, {
883
+ "2": "Default quorum threshold",
884
+ "3": "Stricter quorum",
885
+ "4": "Very strict quorum",
886
+ });
887
+ }
888
+
889
+ // 9) --concurrency value position.
890
+ if (prev === "--concurrency") {
891
+ return wrap(filterList(["1", "2", "3", "4"], tail), headPrefix, {
892
+ "1": "Serial reviewers",
893
+ "2": "Up to 2 in parallel",
894
+ "3": "Up to 3 in parallel",
895
+ "4": "Up to 4 in parallel",
896
+ });
897
+ }
898
+
899
+ // 10) --consensus-model value position: reuse model list when available.
900
+ if (prev === "--consensus-model") {
901
+ const models = deps.models ?? [];
902
+ if (models.length === 0) {
903
+ return [{
904
+ value: `${headPrefix}${tail}`,
905
+ label: tail || "<provider/model>",
906
+ description: "Catalog not loaded; type provider/model or short id",
907
+ }];
908
+ }
909
+ return modelItems(models, sig, headPrefix, tail, deps);
910
+ }
911
+
912
+ // 11) --continue value position: defer (no static list; handler owns it).
483
913
  if (prev === "--continue") return null;
484
914
 
485
- // 5) Flag completion (tail starts with "--").
915
+ // 6) Flag completion (tail starts with "--").
486
916
  if (tail.startsWith("--")) {
487
- return flagItems(head, tail, headPrefix);
917
+ return flagItems(head, tail, headPrefix, strategy);
488
918
  }
489
919
 
490
- // 6) File attachment in progress → defer to built-in file completion.
920
+ // 7) File attachment in progress → defer to built-in file completion.
491
921
  if (tail.startsWith("@")) return null;
492
922
 
493
- // 7) Top-level: scene templates + `models` + flags.
494
- return topLevelCompletions(head, tail, deps, headPrefix, sig);
923
+ // 8) Top-level: strategy-aware templates + bare model hints + flags.
924
+ return topLevelCompletions(head, rawHead, tail, deps, headPrefix, sig);
495
925
  }