@zephyrdeng/pi-review 0.6.0 → 0.6.2
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 +6 -3
- package/extensions/review.ts +35 -1
- package/extensions/rv-completions.test.ts +41 -29
- package/extensions/rv-completions.ts +156 -111
- package/extensions/rv-locale.test.ts +17 -0
- package/extensions/rv-locale.ts +81 -0
- package/extensions/rv-model-priorities.test.ts +69 -0
- package/extensions/rv-model-priorities.ts +138 -0
- package/extensions/rv-prompts.test.ts +9 -0
- package/extensions/rv-prompts.ts +32 -15
- package/extensions/rv-semantic.test.ts +23 -0
- package/extensions/rv-semantic.ts +110 -0
- package/package.json +3 -3
- package/resources/rv-model-priorities.json +17 -0
- package/skills/pi-review/SKILL.md +9 -6
- package/skills/pi-review/references/codex-tools.md +2 -0
- package/skills/pi-review/references/model-selection.md +72 -0
package/README.md
CHANGED
|
@@ -224,9 +224,12 @@ The `/rv` command injects a task message for the parent agent (Pi host rules: de
|
|
|
224
224
|
|
|
225
225
|
Argument completions are context-aware. After the host session starts, `/rv` reads the live model registry and offers:
|
|
226
226
|
|
|
227
|
-
- **Model list** after `--model
|
|
228
|
-
- **Thinking suffix** after `provider/model:` — only levels the chosen model actually supports
|
|
229
|
-
-
|
|
227
|
+
- **Model list** after `--model `: candidates come from the live Pi registry; order follows **`resources/rv-model-priorities.json`** (override with `PI_REVIEW_RV_PRIORITIES`). Presets match registry ids by substring and prefer newer version strings (e.g. kimi `2.7`, `claude-opus-4-8`). Profiles: **code** (e.g. gpt-5.5 `:xhigh`, glm-5.2 `:high`), **frontend** (vue/css/… → kimi, claude-sonnet, minimax-m3), **plan/challenge** (claude-opus-4-8, deepseek-v4-pro, …).
|
|
228
|
+
- **Thinking suffix** after `provider/model:` — only levels the chosen model actually supports.
|
|
229
|
+
- **Semantic phrases** (e.g. `code review` / 代码审核, `查看模型列表`) in addition to flags; orchestration prompts follow **session locale** (zh/en) for summaries.
|
|
230
|
+
- **Scene templates** at the top level (code / frontend / plan presets).
|
|
231
|
+
|
|
232
|
+
**Claude Code / Codex:** the bundled skill includes **[skills/pi-review/references/model-selection.md](skills/pi-review/references/model-selection.md)** — same presets as `/rv` for choosing `--model` after `pi-review models`.
|
|
230
233
|
|
|
231
234
|
Completions are a hint layer only; execution remains skill-driven. When the model registry is unavailable (e.g. non-TUI mode), `/rv` falls back to the static hint list.
|
|
232
235
|
|
package/extensions/review.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
buildRvCompletions,
|
|
4
4
|
type ModelInfo,
|
|
5
5
|
} from "./rv-completions.js";
|
|
6
|
+
import { detectRvLocale } from "./rv-locale.js";
|
|
6
7
|
import {
|
|
7
8
|
buildRvOrchestrationPrompt,
|
|
8
9
|
parseRvArgs,
|
|
@@ -18,6 +19,28 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
let capturedModels: ModelInfo[] | undefined;
|
|
20
21
|
let capturedPrimaryProvider: string | undefined;
|
|
22
|
+
let capturedLocale: ReturnType<typeof detectRvLocale> = "en";
|
|
23
|
+
|
|
24
|
+
function sampleSessionText(
|
|
25
|
+
sessionManager: { getEntries?: () => Array<{ type?: string; content?: unknown }> },
|
|
26
|
+
): string[] {
|
|
27
|
+
const out: string[] = [];
|
|
28
|
+
try {
|
|
29
|
+
const entries = sessionManager.getEntries?.() ?? [];
|
|
30
|
+
for (let i = entries.length - 1; i >= 0 && out.length < 12; i--) {
|
|
31
|
+
const e = entries[i];
|
|
32
|
+
if (e.type !== "message") continue;
|
|
33
|
+
const c = e.content;
|
|
34
|
+
if (typeof c === "string") out.push(c);
|
|
35
|
+
else if (c && typeof c === "object" && "text" in c && typeof (c as { text: string }).text === "string") {
|
|
36
|
+
out.push((c as { text: string }).text);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
/* ignore */
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
21
44
|
|
|
22
45
|
function toModelInfo(m: {
|
|
23
46
|
provider: string;
|
|
@@ -50,12 +73,22 @@ export default function piReviewExtension(pi: ExtensionAPI) {
|
|
|
50
73
|
const available = ctx.modelRegistry?.getAvailable?.() ?? [];
|
|
51
74
|
capturedModels = available.map((m) => toModelInfo(m as unknown as Parameters<typeof toModelInfo>[0]));
|
|
52
75
|
capturedPrimaryProvider = ctx.model?.provider;
|
|
76
|
+
capturedLocale = detectRvLocale(sampleSessionText(ctx.sessionManager));
|
|
53
77
|
} catch {
|
|
54
78
|
capturedModels = undefined;
|
|
55
79
|
capturedPrimaryProvider = undefined;
|
|
80
|
+
capturedLocale = "en";
|
|
56
81
|
}
|
|
57
82
|
});
|
|
58
83
|
|
|
84
|
+
function localeForHandler(ctx: { sessionManager?: Parameters<typeof sampleSessionText>[0] }): typeof capturedLocale {
|
|
85
|
+
try {
|
|
86
|
+
return detectRvLocale(sampleSessionText(ctx.sessionManager ?? { getEntries: () => [] }));
|
|
87
|
+
} catch {
|
|
88
|
+
return capturedLocale;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
pi.registerCommand("rv", {
|
|
60
93
|
description:
|
|
61
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",
|
|
@@ -65,6 +98,7 @@ export default function piReviewExtension(pi: ExtensionAPI) {
|
|
|
65
98
|
const dynamic = buildRvCompletions(prefix, {
|
|
66
99
|
models: capturedModels,
|
|
67
100
|
primaryProvider: capturedPrimaryProvider,
|
|
101
|
+
locale: capturedLocale,
|
|
68
102
|
});
|
|
69
103
|
if (dynamic && dynamic.length) return dynamic;
|
|
70
104
|
}
|
|
@@ -93,7 +127,7 @@ export default function piReviewExtension(pi: ExtensionAPI) {
|
|
|
93
127
|
return;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
|
-
pi.sendUserMessage(buildRvOrchestrationPrompt(parsed));
|
|
130
|
+
pi.sendUserMessage(buildRvOrchestrationPrompt(parsed, localeForHandler(ctx)));
|
|
97
131
|
},
|
|
98
132
|
});
|
|
99
133
|
}
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
fuzzyMatch,
|
|
7
7
|
MODE_HINTS,
|
|
8
8
|
rankModelsForReview,
|
|
9
|
-
scoreModelForReview,
|
|
10
9
|
tokenizeArgPrefix,
|
|
11
10
|
type ModelInfo,
|
|
12
11
|
} from "./rv-completions.js";
|
|
12
|
+
import { rvUi } from "./rv-locale.js";
|
|
13
|
+
import { DEFAULT_REVIEW_MODEL_PRIORITIES } from "./rv-model-priorities.js";
|
|
13
14
|
|
|
14
15
|
function model(partial: Partial<ModelInfo> & Pick<ModelInfo, "provider" | "id">): ModelInfo {
|
|
15
16
|
return {
|
|
@@ -72,28 +73,23 @@ describe("extractSignals", () => {
|
|
|
72
73
|
assert.equal(sig.targetExt, "ts");
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
it("carries primary provider
|
|
76
|
+
it("carries primary provider on signals", () => {
|
|
76
77
|
const sig = extractSignals([], { primaryProvider: "anthropic" });
|
|
77
78
|
assert.equal(sig.primaryProvider, "anthropic");
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
describe("
|
|
82
|
-
it("
|
|
83
|
-
const sig =
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const flash = scoreModelForReview(MODELS[2], sig);
|
|
87
|
-
assert.ok(gpt.score > opus.score, `gpt (${gpt.score}) should beat opus (${opus.score}) for challenge+cross-vendor`);
|
|
88
|
-
assert.ok(opus.score > flash.score, "opus should beat the non-reasoning flash");
|
|
89
|
-
assert.ok(flash.score >= 2, "flash still gets context-window points");
|
|
82
|
+
describe("rankModelsForReview", () => {
|
|
83
|
+
it("puts preset code models first when registered", () => {
|
|
84
|
+
const sig = extractSignals(["@src/a.ts"], {});
|
|
85
|
+
const ranked = rankModelsForReview(MODELS, sig, DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
86
|
+
assert.equal(ranked[0].id, "gpt-5.5");
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
it("
|
|
93
|
-
const sig =
|
|
94
|
-
const ranked = rankModelsForReview(MODELS, sig);
|
|
95
|
-
assert.equal(ranked[0].id, "
|
|
96
|
-
assert.equal(ranked[ranked.length - 1].id, "gemini-3-flash");
|
|
89
|
+
it("puts preset plan models first for plan mode", () => {
|
|
90
|
+
const sig = extractSignals(["--mode", "plan"], {});
|
|
91
|
+
const ranked = rankModelsForReview(MODELS, sig, DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
92
|
+
assert.equal(ranked[0].id, "claude-opus-4-6");
|
|
97
93
|
});
|
|
98
94
|
});
|
|
99
95
|
|
|
@@ -106,13 +102,17 @@ describe("fuzzyMatch", () => {
|
|
|
106
102
|
});
|
|
107
103
|
|
|
108
104
|
describe("buildRvCompletions", () => {
|
|
109
|
-
const deps = {
|
|
105
|
+
const deps = {
|
|
106
|
+
models: MODELS,
|
|
107
|
+
primaryProvider: "anthropic",
|
|
108
|
+
priorities: DEFAULT_REVIEW_MODEL_PRIORITIES,
|
|
109
|
+
};
|
|
110
110
|
|
|
111
111
|
it("returns dynamic model list after --model, with recommended marker on top items", () => {
|
|
112
112
|
const items = buildRvCompletions("@x.md --model ", deps);
|
|
113
113
|
assert.ok(items, "expected items");
|
|
114
114
|
assert.ok(items!.some((i) => i.label === "openai/gpt-5.5"));
|
|
115
|
-
const top = items!.find((i) => i.description?.includes("
|
|
115
|
+
const top = items!.find((i) => i.description?.includes("Suggested"));
|
|
116
116
|
assert.ok(top, "expected at least one recommended item");
|
|
117
117
|
// value carries the head so applyCompletion replaces the whole arg safely
|
|
118
118
|
assert.ok(top!.value.startsWith("@x.md --model "));
|
|
@@ -169,14 +169,29 @@ describe("buildRvCompletions", () => {
|
|
|
169
169
|
|
|
170
170
|
it("offers scene templates + models keyword at empty top level", () => {
|
|
171
171
|
const items = buildRvCompletions("", deps);
|
|
172
|
+
const ui = rvUi("en");
|
|
172
173
|
assert.ok(items);
|
|
173
|
-
assert.ok(items!.some((i) => i.label ===
|
|
174
|
-
assert.ok(items!.some((i) => i.label.
|
|
175
|
-
|
|
176
|
-
const tmpl = items!.find((i) => i.label.startsWith("审代码改动"))!;
|
|
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
177
|
assert.ok(tmpl.value.endsWith(" @"));
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it("does not duplicate list-models completion at top level", () => {
|
|
181
|
+
for (const locale of ["en", "zh"] as const) {
|
|
182
|
+
for (const prefix of ["", "models", "list", "mod"]) {
|
|
183
|
+
const items = buildRvCompletions(prefix, { ...deps, locale });
|
|
184
|
+
if (!items?.length) continue;
|
|
185
|
+
const ui = rvUi(locale);
|
|
186
|
+
const listModelItems = items.filter((i) => i.label === ui.listModels);
|
|
187
|
+
assert.ok(
|
|
188
|
+
listModelItems.length <= 1,
|
|
189
|
+
`locale=${locale} prefix=${JSON.stringify(prefix)} got ${listModelItems.length} "${ui.listModels}" items`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
180
195
|
it("every returned value preserves the head prefix (整段替换安全)", () => {
|
|
181
196
|
const cases = ["@a.ts --mode ", "@a.ts --model ", "@a.ts --thinking ", "@a.ts --"];
|
|
182
197
|
for (const c of cases) {
|
|
@@ -202,15 +217,12 @@ describe("buildRvCompletions", () => {
|
|
|
202
217
|
}
|
|
203
218
|
});
|
|
204
219
|
|
|
205
|
-
it("scene templates
|
|
220
|
+
it("omits preset scene templates when registry has no preset id match", () => {
|
|
206
221
|
const nonReasoning = [{ ...MODELS[2], thinkingLevels: [] }] as ModelInfo[];
|
|
207
|
-
const items = buildRvCompletions("", { models: nonReasoning });
|
|
222
|
+
const items = buildRvCompletions("", { models: nonReasoning, priorities: DEFAULT_REVIEW_MODEL_PRIORITIES });
|
|
208
223
|
assert.ok(items);
|
|
209
|
-
|
|
210
|
-
assert.ok(
|
|
211
|
-
// Must NOT contain a dangling colon like `gemini-3-flash: @`
|
|
212
|
-
assert.doesNotMatch(tmpl!.value, /flash:\s@/);
|
|
213
|
-
assert.match(tmpl!.value, /flash @/);
|
|
224
|
+
assert.ok(!items!.some((i) => i.label === rvUi("en").codePreset));
|
|
225
|
+
assert.ok(items!.some((i) => i.label === rvUi("en").listModels));
|
|
214
226
|
});
|
|
215
227
|
|
|
216
228
|
it("falls back gracefully when models are unavailable (static flags still work)", () => {
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure, testable /rv argument-completion logic.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* across hosts. The thin runtime adapter (registry capture + AutocompleteItem
|
|
7
|
-
* shaping) lives in `review.ts`.
|
|
8
|
-
*
|
|
9
|
-
* Completion items are returned "续写式": `value` always carries the already-typed
|
|
10
|
-
* head tokens because pi-tui's `applyCompletion` replaces the *entire* argument
|
|
11
|
-
* text after the slash command with `item.value`.
|
|
4
|
+
* Avoids pi types so tokenizer + preset ordering can be unit-tested.
|
|
5
|
+
* Completion `value` must include the typed head (pi-tui replaces the whole arg).
|
|
12
6
|
*/
|
|
13
7
|
|
|
8
|
+
import { type RvLocale, rvUi } from "./rv-locale.js";
|
|
9
|
+
import { semanticCompletionItems, SEMANTIC_PHRASES } from "./rv-semantic.js";
|
|
10
|
+
import {
|
|
11
|
+
loadReviewModelPriorities,
|
|
12
|
+
primaryPresetForProfile,
|
|
13
|
+
rankModelsWithPresets,
|
|
14
|
+
resolvePresetOrderedModels,
|
|
15
|
+
resolveReviewProfile,
|
|
16
|
+
type ReviewModelPriorities,
|
|
17
|
+
type ReviewProfile,
|
|
18
|
+
} from "./rv-model-priorities.js";
|
|
19
|
+
|
|
14
20
|
export type AutocompleteItem = {
|
|
15
21
|
value: string;
|
|
16
22
|
label: string;
|
|
@@ -36,18 +42,18 @@ export const RV_FLAGS = [
|
|
|
36
42
|
] as const;
|
|
37
43
|
|
|
38
44
|
export const MODE_HINTS: Record<string, string> = {
|
|
39
|
-
code: "
|
|
40
|
-
plan: "
|
|
41
|
-
challenge: "
|
|
45
|
+
code: "Code / diff / MR review",
|
|
46
|
+
plan: "Architecture or plan review",
|
|
47
|
+
challenge: "Adversarial plan review",
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export const FLAG_HINTS: Record<string, string> = {
|
|
45
|
-
"--mode": "
|
|
51
|
+
"--mode": "Review mode: code | plan | challenge",
|
|
46
52
|
"--model": "provider/model[:thinking]",
|
|
47
|
-
"--thinking": "off|minimal|low|medium|high|xhigh",
|
|
48
|
-
"--keep-session": "
|
|
49
|
-
"--no-stream": "
|
|
50
|
-
"--continue": "
|
|
53
|
+
"--thinking": "off | minimal | low | medium | high | xhigh",
|
|
54
|
+
"--keep-session": "Keep session for /rv --continue follow-up",
|
|
55
|
+
"--no-stream": "Buffer output until exit (not default)",
|
|
56
|
+
"--continue": "Resume an existing review session",
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
/** Minimal model metadata we need; converted from the host's Model registry. */
|
|
@@ -74,6 +80,31 @@ export interface ReviewSignals {
|
|
|
74
80
|
export interface CompletionDeps {
|
|
75
81
|
models?: ModelInfo[];
|
|
76
82
|
primaryProvider?: string;
|
|
83
|
+
priorities?: ReviewModelPriorities;
|
|
84
|
+
locale?: RvLocale;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function localeOf(deps: CompletionDeps): RvLocale {
|
|
88
|
+
return deps.locale ?? "en";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function modeFromSemanticHead(head: string[]): string | undefined {
|
|
92
|
+
const joined = head.join(" ").toLowerCase();
|
|
93
|
+
for (const row of SEMANTIC_PHRASES) {
|
|
94
|
+
if (!row.apply.mode) continue;
|
|
95
|
+
for (const p of row.phrases) {
|
|
96
|
+
if (joined.includes(p.toLowerCase())) return row.apply.mode;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getPriorities(deps: CompletionDeps): ReviewModelPriorities {
|
|
103
|
+
return deps.priorities ?? loadReviewModelPriorities();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function reviewProfileForSignals(sig: ReviewSignals): ReviewProfile {
|
|
107
|
+
return resolveReviewProfile(sig.mode, sig.targetExt);
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
// ---------------------------------------------------------------------------
|
|
@@ -111,12 +142,6 @@ export function tokenizeArgPrefix(prefix: string): TokenizedArgs {
|
|
|
111
142
|
// Signal extraction
|
|
112
143
|
// ---------------------------------------------------------------------------
|
|
113
144
|
|
|
114
|
-
const CODE_EXTS = new Set([
|
|
115
|
-
"ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "rb", "go", "rs", "java", "kt",
|
|
116
|
-
"c", "cc", "cpp", "h", "hpp", "cs", "php", "swift", "scala", "sh", "sql",
|
|
117
|
-
"vue", "svelte",
|
|
118
|
-
]);
|
|
119
|
-
const PLAN_EXTS = new Set(["md", "markdown", "txt", "rst", "adoc"]);
|
|
120
145
|
|
|
121
146
|
function extOf(token: string): string | undefined {
|
|
122
147
|
const cleaned = token.replace(/^['"]|['"]$/g, "").replace(/^@/, "");
|
|
@@ -127,7 +152,8 @@ function extOf(token: string): string | undefined {
|
|
|
127
152
|
export function extractSignals(head: string[], deps: CompletionDeps): ReviewSignals {
|
|
128
153
|
const target = head.find((t) => t.startsWith("@"));
|
|
129
154
|
const modeIdx = head.indexOf("--mode");
|
|
130
|
-
const mode =
|
|
155
|
+
const mode =
|
|
156
|
+
(modeIdx >= 0 ? head[modeIdx + 1] : undefined) ?? modeFromSemanticHead(head) ?? "code";
|
|
131
157
|
return {
|
|
132
158
|
mode,
|
|
133
159
|
hasTarget: Boolean(target),
|
|
@@ -137,46 +163,27 @@ export function extractSignals(head: string[], deps: CompletionDeps): ReviewSign
|
|
|
137
163
|
}
|
|
138
164
|
|
|
139
165
|
// ---------------------------------------------------------------------------
|
|
140
|
-
// Model
|
|
166
|
+
// Model ordering: registry + preset priorities (no LLM)
|
|
141
167
|
// ---------------------------------------------------------------------------
|
|
142
168
|
|
|
143
|
-
export function scoreModelForReview(
|
|
144
|
-
m: ModelInfo,
|
|
145
|
-
sig: ReviewSignals,
|
|
146
|
-
): { score: number; reason: string } {
|
|
147
|
-
let score = 0;
|
|
148
|
-
const reasons: string[] = [];
|
|
149
|
-
if (m.reasoning) {
|
|
150
|
-
score += 3;
|
|
151
|
-
reasons.push("reasoning");
|
|
152
|
-
}
|
|
153
|
-
if (m.contextWindow >= 200_000) score += 2;
|
|
154
|
-
else if (m.contextWindow >= 100_000) score += 1;
|
|
155
|
-
if (sig.primaryProvider && m.provider !== sig.primaryProvider) {
|
|
156
|
-
score += 1;
|
|
157
|
-
reasons.push("跨厂商交叉审核");
|
|
158
|
-
}
|
|
159
|
-
if ((sig.mode === "plan" || sig.mode === "challenge") && m.thinkingLevels.includes("xhigh")) {
|
|
160
|
-
score += 2;
|
|
161
|
-
reasons.push("xhigh 思考");
|
|
162
|
-
} else if (m.thinkingLevels.includes("high")) {
|
|
163
|
-
score += 1;
|
|
164
|
-
}
|
|
165
|
-
if (sig.targetExt && PLAN_EXTS.has(sig.targetExt) && m.reasoning) {
|
|
166
|
-
score += 1;
|
|
167
|
-
reasons.push("适合文档评审");
|
|
168
|
-
}
|
|
169
|
-
return { score, reason: reasons.filter(Boolean).join(" · ") };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
169
|
export function rankModelsForReview(
|
|
173
170
|
models: ModelInfo[],
|
|
174
171
|
sig: ReviewSignals,
|
|
172
|
+
priorities?: ReviewModelPriorities,
|
|
175
173
|
): ModelInfo[] {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
const profile = reviewProfileForSignals(sig);
|
|
175
|
+
return rankModelsWithPresets(models, profile, priorities ?? loadReviewModelPriorities());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function presetDescription(
|
|
179
|
+
presetLabel: string,
|
|
180
|
+
profile: ReviewProfile,
|
|
181
|
+
presetRank: number | undefined,
|
|
182
|
+
locale: RvLocale,
|
|
183
|
+
): string {
|
|
184
|
+
if (presetRank === undefined) return "";
|
|
185
|
+
const ui = rvUi(locale);
|
|
186
|
+
return `${ui.presetTier(presetRank)} · ${profile} · ${presetLabel}`;
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
// ---------------------------------------------------------------------------
|
|
@@ -234,16 +241,16 @@ function wrap(
|
|
|
234
241
|
}));
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
function thinkingDescription(level: string, sig: ReviewSignals): string {
|
|
244
|
+
function thinkingDescription(level: string, sig: ReviewSignals, locale: RvLocale): string {
|
|
238
245
|
const recommended = sig.mode === "code" ? "high" : "xhigh";
|
|
239
|
-
const star = level === recommended ?
|
|
246
|
+
const star = level === recommended ? rvUi(locale).thinkingSuggested : "";
|
|
240
247
|
const map: Record<string, string> = {
|
|
241
|
-
off: "
|
|
242
|
-
minimal: "
|
|
243
|
-
low: "
|
|
244
|
-
medium: "
|
|
245
|
-
high: "
|
|
246
|
-
xhigh: "
|
|
248
|
+
off: "No thinking (fastest)",
|
|
249
|
+
minimal: "Minimal thinking",
|
|
250
|
+
low: "Low effort",
|
|
251
|
+
medium: "Medium effort",
|
|
252
|
+
high: "High effort (common for review)",
|
|
253
|
+
xhigh: "Max effort (plan/challenge)",
|
|
247
254
|
};
|
|
248
255
|
return `${star}${map[level] ?? level}`;
|
|
249
256
|
}
|
|
@@ -259,22 +266,25 @@ function modelItems(
|
|
|
259
266
|
sig: ReviewSignals,
|
|
260
267
|
headPrefix: string,
|
|
261
268
|
tail: string,
|
|
269
|
+
deps: CompletionDeps,
|
|
262
270
|
): AutocompleteItem[] | null {
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
const priorities = getPriorities(deps);
|
|
272
|
+
const profile = reviewProfileForSignals(sig);
|
|
273
|
+
const presetRows = resolvePresetOrderedModels(models, profile, priorities);
|
|
274
|
+
const presetByLabel = new Map(presetRows.map((p) => [p.model.label, p]));
|
|
275
|
+
const ranked = rankModelsWithPresets(models, profile, priorities);
|
|
276
|
+
const items = ranked.map((m) => {
|
|
277
|
+
const preset = presetByLabel.get(m.label);
|
|
278
|
+
const tag = profile;
|
|
269
279
|
return {
|
|
270
280
|
value: `${headPrefix}${m.label}`,
|
|
271
281
|
label: m.label,
|
|
272
282
|
description: [
|
|
273
|
-
|
|
283
|
+
preset ? presetDescription(preset.presetLabel, profile, preset.presetRank, localeOf(deps)) : "",
|
|
274
284
|
tag,
|
|
275
285
|
m.reasoning ? "reasoning" : "",
|
|
276
286
|
contextLabel(m.contextWindow),
|
|
277
|
-
|
|
287
|
+
preset?.thinking ? `preset thinking ${preset.thinking}` : "",
|
|
278
288
|
]
|
|
279
289
|
.filter(Boolean)
|
|
280
290
|
.join(" · "),
|
|
@@ -301,6 +311,7 @@ function thinkingSuffixItems(
|
|
|
301
311
|
models: ModelInfo[],
|
|
302
312
|
headPrefix: string,
|
|
303
313
|
sig: ReviewSignals,
|
|
314
|
+
deps: CompletionDeps,
|
|
304
315
|
): AutocompleteItem[] | null {
|
|
305
316
|
const colonIdx = tail.lastIndexOf(":");
|
|
306
317
|
const modelPart = tail.slice(0, colonIdx);
|
|
@@ -319,40 +330,64 @@ function thinkingSuffixItems(
|
|
|
319
330
|
return filtered.map((level) => ({
|
|
320
331
|
value: `${headPrefix}${modelPart}:${level}`,
|
|
321
332
|
label: level,
|
|
322
|
-
description: thinkingDescription(level, sig),
|
|
333
|
+
description: thinkingDescription(level, sig, localeOf(deps)),
|
|
323
334
|
}));
|
|
324
335
|
}
|
|
325
336
|
|
|
337
|
+
function thinkingSuffixForModel(m: ModelInfo, want?: string): string {
|
|
338
|
+
const lvl =
|
|
339
|
+
want && m.thinkingLevels.includes(want)
|
|
340
|
+
? want
|
|
341
|
+
: want && m.thinkingLevels.includes("high")
|
|
342
|
+
? "high"
|
|
343
|
+
: m.thinkingLevels.includes("xhigh")
|
|
344
|
+
? "xhigh"
|
|
345
|
+
: m.thinkingLevels.includes("high")
|
|
346
|
+
? "high"
|
|
347
|
+
: (m.thinkingLevels[0] ?? "");
|
|
348
|
+
return lvl ? `:${lvl}` : "";
|
|
349
|
+
}
|
|
350
|
+
|
|
326
351
|
function sceneTemplates(
|
|
327
|
-
|
|
352
|
+
models: ModelInfo[],
|
|
353
|
+
sig: ReviewSignals,
|
|
328
354
|
headPrefix: string,
|
|
329
355
|
tail: string,
|
|
356
|
+
deps: CompletionDeps,
|
|
330
357
|
): AutocompleteItem[] {
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
358
|
+
const priorities = getPriorities(deps);
|
|
359
|
+
const codePrimary = primaryPresetForProfile(models, "code", priorities);
|
|
360
|
+
const planPrimary = primaryPresetForProfile(models, "plan", priorities);
|
|
361
|
+
const frontendPrimary = primaryPresetForProfile(models, "frontend", priorities);
|
|
362
|
+
|
|
363
|
+
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
|
+
});
|
|
378
|
+
}
|
|
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
|
+
});
|
|
389
|
+
}
|
|
341
390
|
|
|
342
|
-
const templates = [
|
|
343
|
-
{
|
|
344
|
-
cmd: `--mode code --model ${top.label}${suffix("high")} @`,
|
|
345
|
-
label: "审代码改动(推荐配置)",
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
cmd: `--mode plan --model ${top.label}${suffix("high")} @`,
|
|
349
|
-
label: "架构 / 计划评审",
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
cmd: `--mode challenge --keep-session --model ${top.label}${suffix("xhigh")} @`,
|
|
353
|
-
label: "对抗性方案评审(可追问)",
|
|
354
|
-
},
|
|
355
|
-
];
|
|
356
391
|
return templates
|
|
357
392
|
.filter((t) => fuzzyMatch(t.label, tail) || t.cmd.includes(tail))
|
|
358
393
|
.map((t) => ({
|
|
@@ -382,21 +417,31 @@ function topLevelCompletions(
|
|
|
382
417
|
const items: AutocompleteItem[] = [];
|
|
383
418
|
|
|
384
419
|
// Scene templates (only at top level, before any target/mode chosen is fine).
|
|
385
|
-
|
|
386
|
-
|
|
420
|
+
if (deps.models?.length) {
|
|
421
|
+
items.push(...sceneTemplates(deps.models, sig, headPrefix, tail, deps));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
items.push(...semanticCompletionItems(localeOf(deps), tail, headPrefix, head));
|
|
387
425
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
) {
|
|
390
434
|
items.push({
|
|
391
|
-
value: `${headPrefix}
|
|
392
|
-
label:
|
|
393
|
-
description:
|
|
435
|
+
value: `${headPrefix}${ui.listModels}`,
|
|
436
|
+
label: ui.listModels,
|
|
437
|
+
description: ui.listModelsDesc,
|
|
394
438
|
});
|
|
395
439
|
}
|
|
396
440
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
441
|
+
if (tail.startsWith("--")) {
|
|
442
|
+
const flags = flagItems(head, tail, headPrefix);
|
|
443
|
+
if (flags) items.push(...flags);
|
|
444
|
+
}
|
|
400
445
|
|
|
401
446
|
return items.length ? items : null;
|
|
402
447
|
}
|
|
@@ -418,10 +463,10 @@ export function buildRvCompletions(
|
|
|
418
463
|
if (prev === "--model") {
|
|
419
464
|
const colonIdx = tail.lastIndexOf(":");
|
|
420
465
|
if (colonIdx !== -1 && tail.slice(0, colonIdx).includes("/")) {
|
|
421
|
-
const items = thinkingSuffixItems(tail, deps.models ?? [], headPrefix, sig);
|
|
466
|
+
const items = thinkingSuffixItems(tail, deps.models ?? [], headPrefix, sig, deps);
|
|
422
467
|
return items;
|
|
423
468
|
}
|
|
424
|
-
return modelItems(deps.models ?? [], sig, headPrefix, tail);
|
|
469
|
+
return modelItems(deps.models ?? [], sig, headPrefix, tail, deps);
|
|
425
470
|
}
|
|
426
471
|
|
|
427
472
|
// 2) --mode value position.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { detectRvLocale } from "./rv-locale.js";
|
|
4
|
+
|
|
5
|
+
describe("detectRvLocale", () => {
|
|
6
|
+
it("returns zh when recent messages are mostly Chinese", () => {
|
|
7
|
+
assert.equal(detectRvLocale(["帮我看一下这个方案", "好的"]), "zh");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns en for English-only samples", () => {
|
|
11
|
+
assert.equal(detectRvLocale(["Please review this diff", "Thanks"]), "en");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("defaults to en when empty", () => {
|
|
15
|
+
assert.equal(detectRvLocale([]), "en");
|
|
16
|
+
});
|
|
17
|
+
});
|