@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
|
@@ -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
|
|
294
|
-
//
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
label: ui.
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
label
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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,
|
|
819
|
+
const items = thinkingSuffixItems(tail, models, headPrefix, sig, deps);
|
|
467
820
|
return items;
|
|
468
821
|
}
|
|
469
|
-
return modelItems(
|
|
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) --
|
|
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
|
-
//
|
|
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
|
-
//
|
|
920
|
+
// 7) File attachment in progress → defer to built-in file completion.
|
|
491
921
|
if (tail.startsWith("@")) return null;
|
|
492
922
|
|
|
493
|
-
//
|
|
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
|
}
|