@zephyrdeng/pi-review 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.map +1 -1
- package/dist/update.js +48 -19
- package/dist/update.js.map +1 -1
- package/extensions/panel-tool.test.ts +366 -8
- package/extensions/panel-tool.ts +289 -68
- package/extensions/review.test.ts +79 -0
- package/extensions/review.ts +188 -44
- package/extensions/rv-completions.test.ts +177 -18
- package/extensions/rv-completions.ts +490 -60
- package/extensions/rv-interactive.test.ts +562 -0
- package/extensions/rv-interactive.ts +616 -0
- package/extensions/rv-locale.test.ts +12 -4
- package/extensions/rv-locale.ts +44 -11
- package/extensions/rv-model-picker.test.ts +172 -0
- package/extensions/rv-model-picker.ts +313 -0
- package/extensions/rv-model-priorities.test.ts +29 -9
- package/extensions/rv-model-priorities.ts +23 -8
- package/extensions/rv-prompts.test.ts +274 -2
- package/extensions/rv-prompts.ts +479 -33
- package/extensions/rv-resolve.test.ts +189 -0
- package/extensions/rv-resolve.ts +313 -0
- package/extensions/rv-semantic.test.ts +1 -1
- package/extensions/rv-semantic.ts +38 -4
- package/package.json +7 -4
- package/resources/rv-model-priorities.json +18 -9
- package/skills/pi-review/SKILL.md +84 -60
- package/skills/pi-review/references/codex-tools.md +18 -11
- package/skills/pi-review/references/model-selection.md +49 -28
- package/dist/extensions/review.d.ts +0 -2
- package/dist/extensions/review.js +0 -22
- package/dist/extensions/rv-prompts.d.ts +0 -19
- package/dist/extensions/rv-prompts.js +0 -132
package/extensions/rv-locale.ts
CHANGED
|
@@ -4,7 +4,23 @@ export type RvLocale = "en" | "zh";
|
|
|
4
4
|
|
|
5
5
|
const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf]/;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function systemPrefersZh(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
8
|
+
const keys = ["LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"];
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const value = env[key];
|
|
11
|
+
if (!value) continue;
|
|
12
|
+
if (/(^|[_\.\-])zh/i.test(value)) return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prefer Chinese when:
|
|
19
|
+
* - recent session text has meaningful CJK, or
|
|
20
|
+
* - system locale is zh and samples are empty/ambiguous.
|
|
21
|
+
* Re-run this on each completion; session_start-only capture freezes English too early.
|
|
22
|
+
*/
|
|
23
|
+
export function detectRvLocale(textSamples: string[], env: NodeJS.ProcessEnv = process.env): RvLocale {
|
|
8
24
|
let cjk = 0;
|
|
9
25
|
let latin = 0;
|
|
10
26
|
for (const sample of textSamples) {
|
|
@@ -14,8 +30,10 @@ export function detectRvLocale(textSamples: string[]): RvLocale {
|
|
|
14
30
|
else if (/[a-zA-Z]/.test(ch)) latin++;
|
|
15
31
|
}
|
|
16
32
|
}
|
|
17
|
-
if (cjk
|
|
18
|
-
|
|
33
|
+
if (cjk > 0 && cjk >= Math.max(1, latin * 0.05)) return "zh";
|
|
34
|
+
if (cjk === 0 && latin === 0) return systemPrefersZh(env) ? "zh" : "en";
|
|
35
|
+
if (systemPrefersZh(env) && cjk > 0) return "zh";
|
|
36
|
+
return "en";
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
export type RvUiStrings = {
|
|
@@ -32,6 +50,11 @@ export type RvUiStrings = {
|
|
|
32
50
|
frontendPreset: string;
|
|
33
51
|
planPreset: string;
|
|
34
52
|
challengePreset: string;
|
|
53
|
+
loopCodePreset: string;
|
|
54
|
+
loopCodeDesc: string;
|
|
55
|
+
loopTwoRounds: string;
|
|
56
|
+
loopPlanPreset: string;
|
|
57
|
+
presetHint: string;
|
|
35
58
|
suggested: string;
|
|
36
59
|
presetTier: (n: number) => string;
|
|
37
60
|
thinkingSuggested: string;
|
|
@@ -48,10 +71,15 @@ const UI: Record<RvLocale, RvUiStrings> = {
|
|
|
48
71
|
keepSessionDesc: "Persist review session for /rv --continue",
|
|
49
72
|
listModels: "list models",
|
|
50
73
|
listModelsDesc: "Show provider/model catalog",
|
|
51
|
-
codePreset: "Code review
|
|
52
|
-
frontendPreset: "Frontend
|
|
53
|
-
planPreset: "Plan review
|
|
54
|
-
challengePreset: "Challenge
|
|
74
|
+
codePreset: "Code review preset",
|
|
75
|
+
frontendPreset: "Frontend preset",
|
|
76
|
+
planPreset: "Plan review preset",
|
|
77
|
+
challengePreset: "Challenge preset",
|
|
78
|
+
loopCodePreset: "Loop closeout (code)",
|
|
79
|
+
loopCodeDesc: "Host fixes, re-review until clean",
|
|
80
|
+
loopTwoRounds: "Loop closeout (2 rounds)",
|
|
81
|
+
loopPlanPreset: "Loop closeout (plan)",
|
|
82
|
+
presetHint: "Type preset / code / plan for templates",
|
|
55
83
|
suggested: "Suggested",
|
|
56
84
|
presetTier: (n) => (n === 0 ? "Suggested" : `Preset #${n + 1}`),
|
|
57
85
|
thinkingSuggested: "Suggested ",
|
|
@@ -66,10 +94,15 @@ const UI: Record<RvLocale, RvUiStrings> = {
|
|
|
66
94
|
keepSessionDesc: "保留 review 会话,便于 /rv --continue",
|
|
67
95
|
listModels: "查看模型列表",
|
|
68
96
|
listModelsDesc: "列出当前可用 provider/model",
|
|
69
|
-
codePreset: "
|
|
70
|
-
frontendPreset: "
|
|
71
|
-
planPreset: "
|
|
72
|
-
challengePreset: "
|
|
97
|
+
codePreset: "代码审核预设",
|
|
98
|
+
frontendPreset: "前端审核预设",
|
|
99
|
+
planPreset: "方案审核预设",
|
|
100
|
+
challengePreset: "对抗审核预设",
|
|
101
|
+
loopCodePreset: "Loop 关单(代码)",
|
|
102
|
+
loopCodeDesc: "宿主修 → 再审,直到 clean",
|
|
103
|
+
loopTwoRounds: "Loop 两轮关单",
|
|
104
|
+
loopPlanPreset: "Loop 关单(方案)",
|
|
105
|
+
presetHint: "输入 预设 / 代码 / 方案 查看模板",
|
|
73
106
|
suggested: "推荐",
|
|
74
107
|
presetTier: (n) => (n === 0 ? "推荐" : `预设 #${n + 1}`),
|
|
75
108
|
thinkingSuggested: "推荐 ",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ModelInfo } from "./rv-completions.js";
|
|
6
|
+
import {
|
|
7
|
+
MODEL_PICKER_SKIP,
|
|
8
|
+
RvModelPickerComponent,
|
|
9
|
+
pickerProviders,
|
|
10
|
+
pickerRows,
|
|
11
|
+
type ModelPickerResult,
|
|
12
|
+
} from "./rv-model-picker.js";
|
|
13
|
+
|
|
14
|
+
// A fixture that mirrors the user's example: a `zenmux` provider with glm-5.2,
|
|
15
|
+
// plus a couple of other providers so scope cycling and multi-token search are
|
|
16
|
+
// meaningful. Ranking is driven by the caller (we pass already-ranked input).
|
|
17
|
+
const ranked: ModelInfo[] = [
|
|
18
|
+
{ provider: "anthropic", id: "claude-sonnet-4-5", label: "anthropic/claude-sonnet-4-5", name: "sonnet", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
19
|
+
{ provider: "zenmux", id: "glm-5.2", label: "zenmux/glm-5.2", name: "glm", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
20
|
+
{ provider: "zenmux", id: "glm-5.2-flash", label: "zenmux/glm-5.2-flash", name: "glm-flash", reasoning: true, contextWindow: 200000, thinkingLevels: ["high"] },
|
|
21
|
+
{ provider: "openai", id: "gpt-5.6-luna", label: "openai/gpt-5.6-luna", name: "luna", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
22
|
+
{ provider: "google", id: "gemini-3-pro", label: "google/gemini-3-pro", name: "pro", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Minimal fake TUI: the component only calls tui.requestRender().
|
|
26
|
+
const fakeTui = { requestRender: () => {} } as unknown as Parameters<typeof RvModelPickerComponent.create>[0];
|
|
27
|
+
const kb = getKeybindings() as KeybindingsManager;
|
|
28
|
+
|
|
29
|
+
const theme = {
|
|
30
|
+
fg: (_c: string, t: string) => t,
|
|
31
|
+
bold: (t: string) => t,
|
|
32
|
+
} as unknown as Parameters<typeof RvModelPickerComponent.create>[1];
|
|
33
|
+
|
|
34
|
+
function makePicker(allowSkip: boolean, done: (r: ModelPickerResult) => void) {
|
|
35
|
+
return RvModelPickerComponent.create(fakeTui, theme, kb, done, {
|
|
36
|
+
locale: "en",
|
|
37
|
+
ranked,
|
|
38
|
+
allowSkip,
|
|
39
|
+
title: "Model",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderText(picker: RvModelPickerComponent, width = 80): string {
|
|
44
|
+
return picker.render(width).join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Raw terminal bytes the real keybindings layer understands.
|
|
48
|
+
const KEY = {
|
|
49
|
+
up: "\x1b[A",
|
|
50
|
+
down: "\x1b[B",
|
|
51
|
+
enter: "\r",
|
|
52
|
+
escape: "\x1b",
|
|
53
|
+
tab: "\t",
|
|
54
|
+
bs: "\x7f",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
test("pickerRows prepends the skip row and preserves rank order", () => {
|
|
58
|
+
const withSkip = pickerRows(ranked, true);
|
|
59
|
+
assert.equal(withSkip[0].kind, "skip");
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
withSkip.slice(1).map((r) => (r.kind === "model" ? r.label : "")),
|
|
62
|
+
ranked.map((m) => m.label),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const noSkip = pickerRows(ranked, false);
|
|
66
|
+
assert.equal(noSkip[0].kind, "model");
|
|
67
|
+
assert.equal(noSkip.find((r) => r.kind === "skip"), undefined);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("pickerProviders returns distinct providers in rank order", () => {
|
|
71
|
+
assert.deepEqual(pickerProviders(ranked), ["anthropic", "zenmux", "openai", "google"]);
|
|
72
|
+
assert.deepEqual(pickerProviders([ranked[1], ranked[3]]), ["zenmux", "openai"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("typing filters the list live (single token)", () => {
|
|
76
|
+
const picker = makePicker(false, () => {});
|
|
77
|
+
picker.handleInput("g");
|
|
78
|
+
picker.handleInput("l");
|
|
79
|
+
picker.handleInput("m");
|
|
80
|
+
const out = renderText(picker);
|
|
81
|
+
// glm-5.2 and glm-5.2-flash both match; claude/gpt/gemini do not.
|
|
82
|
+
assert.ok(out.includes("glm-5.2"), out);
|
|
83
|
+
assert.ok(out.includes("glm-5.2-flash"), out);
|
|
84
|
+
assert.ok(!out.includes("claude-sonnet"), out);
|
|
85
|
+
assert.ok(!out.includes("gpt-5.6-luna"), out);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("multi-token fuzzy search matches provider + model (zenmux glm)", () => {
|
|
89
|
+
const picker = makePicker(false, () => {});
|
|
90
|
+
for (const ch of "zenmux glm") picker.handleInput(ch);
|
|
91
|
+
const out = renderText(picker);
|
|
92
|
+
assert.ok(out.includes("glm-5.2"), out);
|
|
93
|
+
assert.ok(out.includes("glm-5.2-flash"), out);
|
|
94
|
+
// openai/gemini/anthropic excluded — different provider
|
|
95
|
+
assert.ok(!out.includes("gpt-5.6-luna"), out);
|
|
96
|
+
assert.ok(!out.includes("gemini-3-pro"), out);
|
|
97
|
+
assert.ok(!out.includes("claude-sonnet"), out);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("no matches renders a no-match line", () => {
|
|
101
|
+
const picker = makePicker(false, () => {});
|
|
102
|
+
for (const ch of "zzzzz") picker.handleInput(ch);
|
|
103
|
+
const out = renderText(picker);
|
|
104
|
+
assert.ok(/No matching models/.test(out), out);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("backspace edits the query and re-filters", () => {
|
|
108
|
+
const picker = makePicker(false, () => {});
|
|
109
|
+
for (const ch of "glm-5.2-") picker.handleInput(ch);
|
|
110
|
+
// typed "glm-5.2-" which matches only glm-5.2-flash (prefix); backspace -> "glm-5.2"
|
|
111
|
+
let out = renderText(picker);
|
|
112
|
+
assert.ok(out.includes("glm-5.2-flash"), out);
|
|
113
|
+
picker.handleInput(KEY.bs);
|
|
114
|
+
out = renderText(picker);
|
|
115
|
+
assert.ok(out.includes("glm-5.2"), out);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("down + enter confirms the highlighted model label", () => {
|
|
119
|
+
let result: ModelPickerResult = "__pending__";
|
|
120
|
+
const picker = makePicker(false, (r) => {
|
|
121
|
+
result = r;
|
|
122
|
+
});
|
|
123
|
+
// filter to glm-5.2 / glm-5.2-flash, then move down to glm-5.2-flash and confirm
|
|
124
|
+
for (const ch of "glm") picker.handleInput(ch);
|
|
125
|
+
picker.handleInput(KEY.down);
|
|
126
|
+
picker.handleInput(KEY.enter);
|
|
127
|
+
assert.equal(result, "zenmux/glm-5.2-flash");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("escape cancels and resolves to undefined", () => {
|
|
131
|
+
let result: ModelPickerResult = "__pending__";
|
|
132
|
+
const picker = makePicker(false, (r) => {
|
|
133
|
+
result = r;
|
|
134
|
+
});
|
|
135
|
+
picker.handleInput(KEY.escape);
|
|
136
|
+
assert.equal(result, undefined);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("skip row is confirmable when allowSkip, resolving to the skip sentinel", () => {
|
|
140
|
+
let result: ModelPickerResult = "__pending__";
|
|
141
|
+
const picker = makePicker(true, (r) => {
|
|
142
|
+
result = r;
|
|
143
|
+
});
|
|
144
|
+
// initial selection is the skip row (index 0); confirm immediately
|
|
145
|
+
picker.handleInput(KEY.enter);
|
|
146
|
+
assert.equal(result, MODEL_PICKER_SKIP);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("Tab cycles provider scope: all -> anthropic -> zenmux -> openai -> google -> all", () => {
|
|
150
|
+
const picker = makePicker(false, () => {});
|
|
151
|
+
const scopeLine = () => renderText(picker).split("\n").find((l) => l.startsWith("Scope:"))!;
|
|
152
|
+
|
|
153
|
+
// all: every provider is active/muted; "all" itself is the accent (here plain) entry
|
|
154
|
+
let scope = scopeLine();
|
|
155
|
+
assert.match(scope, /all/, scope);
|
|
156
|
+
|
|
157
|
+
picker.handleInput(KEY.tab); // -> anthropic
|
|
158
|
+
scope = scopeLine();
|
|
159
|
+
assert.match(scope, /anthropic/, scope);
|
|
160
|
+
assert.ok(!renderText(picker).includes("gpt-5.6-luna"), "openai model hidden in anthropic scope");
|
|
161
|
+
|
|
162
|
+
picker.handleInput(KEY.tab); // -> zenmux
|
|
163
|
+
assert.match(scopeLine(), /zenmux/, scopeLine());
|
|
164
|
+
assert.ok(renderText(picker).includes("glm-5.2"), "zenmux models shown in zenmux scope");
|
|
165
|
+
assert.ok(!renderText(picker).includes("claude-sonnet"), "anthropic hidden in zenmux scope");
|
|
166
|
+
|
|
167
|
+
picker.handleInput(KEY.tab); // -> openai
|
|
168
|
+
picker.handleInput(KEY.tab); // -> google
|
|
169
|
+
picker.handleInput(KEY.tab); // -> all (wraps)
|
|
170
|
+
assert.match(scopeLine(), /all/, scopeLine());
|
|
171
|
+
assert.ok(renderText(picker).includes("claude-sonnet"), "all scope shows every provider");
|
|
172
|
+
});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline, searchable model picker built on Pi's TUI component primitives.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `ui.select` dialog is a flat list (arrows + enter + escape) with no
|
|
5
|
+
* in-place search or grouping, which is painful for a long model catalog. This
|
|
6
|
+
* component composes `Input` + a filtered list (using `fuzzyFilter`) and is
|
|
7
|
+
* shown via `ctx.ui.custom(...)`, which grants keyboard focus so `handleInput`
|
|
8
|
+
* drives the whole flow:
|
|
9
|
+
*
|
|
10
|
+
* - type → live fuzzy filter (e.g. "zenmux glm" matches `zenmux/glm-5.2`)
|
|
11
|
+
* - ↑↓ / PgUp·PgDn → move selection
|
|
12
|
+
* - Enter → confirm the highlighted model
|
|
13
|
+
* - Tab → cycle the provider scope (all → provider1 → … → all)
|
|
14
|
+
* - Esc → cancel
|
|
15
|
+
*
|
|
16
|
+
* It is a pure Pi-tui `Component`; the host `custom` factory owns focus and
|
|
17
|
+
* input routing. The wizard falls back to the `select`-based picker when
|
|
18
|
+
* `custom` is unavailable (print / RPC modes without a TUI).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
Container,
|
|
23
|
+
Input,
|
|
24
|
+
Spacer,
|
|
25
|
+
Text,
|
|
26
|
+
fuzzyFilter,
|
|
27
|
+
getKeybindings,
|
|
28
|
+
type Component,
|
|
29
|
+
type TUI,
|
|
30
|
+
} from "@earendil-works/pi-tui";
|
|
31
|
+
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
import type { ModelInfo } from "./rv-completions.js";
|
|
33
|
+
import type { RvLocale } from "./rv-locale.js";
|
|
34
|
+
|
|
35
|
+
/** Theme shape we use; the host passes a real `Theme` to the custom factory. */
|
|
36
|
+
type PickerTheme = {
|
|
37
|
+
fg: (color: "accent" | "muted" | "warning" | "success" | "error" | "text", text: string) => string;
|
|
38
|
+
bold: (text: string) => string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Result of the picker: a chosen label, the skip sentinel, or cancel (undefined). */
|
|
42
|
+
export type ModelPickerResult = string | "__skip__" | undefined;
|
|
43
|
+
|
|
44
|
+
export const MODEL_PICKER_SKIP = "__skip__";
|
|
45
|
+
|
|
46
|
+
const MAX_VISIBLE = 12;
|
|
47
|
+
|
|
48
|
+
/** A flattened, selectable row: either a real model or the synthetic skip row. */
|
|
49
|
+
type Row = { kind: "model"; label: string; provider: string; id: string } | { kind: "skip" };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the selectable rows from an already-ranked model list. The caller
|
|
53
|
+
* (wizard) owns preset ranking via `rankModelsWithPresets`; this only flattens
|
|
54
|
+
* and optionally prepends the skip row so it stays a pure UI component.
|
|
55
|
+
*/
|
|
56
|
+
export function pickerRows(ranked: ModelInfo[], allowSkip: boolean): Row[] {
|
|
57
|
+
const rows: Row[] = ranked.map((m) => ({ kind: "model", label: m.label, provider: m.provider, id: m.id }));
|
|
58
|
+
return allowSkip ? [{ kind: "skip" }, ...rows] : rows;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Distinct providers in rank order (first occurrence wins), for Tab scope cycling. */
|
|
62
|
+
export function pickerProviders(ranked: ModelInfo[]): string[] {
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
const out: string[] = [];
|
|
65
|
+
for (const m of ranked) {
|
|
66
|
+
if (!seen.has(m.provider)) {
|
|
67
|
+
seen.add(m.provider);
|
|
68
|
+
out.push(m.provider);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rowSearchText(r: Row): string {
|
|
75
|
+
if (r.kind === "skip") return "skip pi default 跳过";
|
|
76
|
+
return `${r.provider} ${r.id} ${r.label}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Inline searchable model picker component.
|
|
81
|
+
*
|
|
82
|
+
* Construct via `RvModelPickerComponent.create(...)` inside the `custom`
|
|
83
|
+
* factory and return it; the host grants focus and forwards keystrokes to
|
|
84
|
+
* `handleInput`. Call `done` with the chosen label, `MODEL_PICKER_SKIP`, or
|
|
85
|
+
* `undefined` (cancel).
|
|
86
|
+
*/
|
|
87
|
+
export class RvModelPickerComponent implements Component {
|
|
88
|
+
private readonly tui: TUI;
|
|
89
|
+
private readonly theme: PickerTheme;
|
|
90
|
+
private readonly keybindings: KeybindingsManager;
|
|
91
|
+
private readonly done: (result: ModelPickerResult) => void;
|
|
92
|
+
private readonly locale: RvLocale;
|
|
93
|
+
private readonly zh: boolean;
|
|
94
|
+
private readonly allRows: Row[];
|
|
95
|
+
private readonly providers: string[];
|
|
96
|
+
|
|
97
|
+
private readonly root = new Container();
|
|
98
|
+
private readonly searchInput: Input;
|
|
99
|
+
private readonly listContainer = new Container();
|
|
100
|
+
private readonly hint: Text;
|
|
101
|
+
private readonly scopeText: Text;
|
|
102
|
+
|
|
103
|
+
private scope: string = "all"; // "all" or a provider name
|
|
104
|
+
private filteredRows: Row[];
|
|
105
|
+
private selectedIndex = 0;
|
|
106
|
+
|
|
107
|
+
private constructor(opts: {
|
|
108
|
+
tui: TUI;
|
|
109
|
+
theme: PickerTheme;
|
|
110
|
+
keybindings: KeybindingsManager;
|
|
111
|
+
done: (result: ModelPickerResult) => void;
|
|
112
|
+
locale: RvLocale;
|
|
113
|
+
ranked: ModelInfo[];
|
|
114
|
+
allowSkip: boolean;
|
|
115
|
+
title: string;
|
|
116
|
+
}) {
|
|
117
|
+
this.tui = opts.tui;
|
|
118
|
+
this.theme = opts.theme;
|
|
119
|
+
this.keybindings = opts.keybindings;
|
|
120
|
+
this.done = opts.done;
|
|
121
|
+
this.locale = opts.locale;
|
|
122
|
+
this.zh = opts.locale === "zh";
|
|
123
|
+
this.allRows = pickerRows(opts.ranked, opts.allowSkip);
|
|
124
|
+
this.providers = pickerProviders(opts.ranked);
|
|
125
|
+
|
|
126
|
+
this.root.addChild(new Spacer(1));
|
|
127
|
+
this.hint = new Text("", 0, 0);
|
|
128
|
+
this.root.addChild(this.hint);
|
|
129
|
+
this.root.addChild(new Spacer(1));
|
|
130
|
+
|
|
131
|
+
this.searchInput = new Input();
|
|
132
|
+
this.root.addChild(this.searchInput);
|
|
133
|
+
this.root.addChild(new Spacer(1));
|
|
134
|
+
|
|
135
|
+
this.scopeText = new Text("", 0, 0);
|
|
136
|
+
this.root.addChild(this.scopeText);
|
|
137
|
+
this.root.addChild(new Spacer(1));
|
|
138
|
+
|
|
139
|
+
this.root.addChild(this.listContainer);
|
|
140
|
+
this.root.addChild(new Spacer(1));
|
|
141
|
+
|
|
142
|
+
this.filteredRows = this.allRows;
|
|
143
|
+
this.updateHint(opts.title);
|
|
144
|
+
this.updateScopeText();
|
|
145
|
+
this.updateList();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Factory used by the wizard inside `ctx.ui.custom(factory)`. */
|
|
149
|
+
static create(
|
|
150
|
+
tui: TUI,
|
|
151
|
+
theme: PickerTheme,
|
|
152
|
+
keybindings: KeybindingsManager,
|
|
153
|
+
done: (result: ModelPickerResult) => void,
|
|
154
|
+
input: { locale: RvLocale; ranked: ModelInfo[]; allowSkip: boolean; title: string },
|
|
155
|
+
): RvModelPickerComponent {
|
|
156
|
+
return new RvModelPickerComponent({ tui, theme, keybindings, done, ...input });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Component interface -------------------------------------------------
|
|
160
|
+
|
|
161
|
+
render(width: number): string[] {
|
|
162
|
+
return this.root.render(width);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
invalidate(): void {
|
|
166
|
+
this.root.invalidate();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
handleInput(keyData: string): void {
|
|
170
|
+
const kb = this.keybindings;
|
|
171
|
+
if (kb.matches(keyData, "tui.input.tab")) {
|
|
172
|
+
this.cycleScope();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
176
|
+
this.moveSelection(-1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
180
|
+
this.moveSelection(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (kb.matches(keyData, "tui.select.pageUp")) {
|
|
184
|
+
this.moveSelection(-MAX_VISIBLE);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (kb.matches(keyData, "tui.select.pageDown")) {
|
|
188
|
+
this.moveSelection(MAX_VISIBLE);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
192
|
+
this.confirmSelection();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
196
|
+
this.done(undefined);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Everything else (printable chars, backspace, etc.) feeds the search box.
|
|
200
|
+
this.searchInput.handleInput(keyData);
|
|
201
|
+
this.applyFilter(this.searchInput.getValue());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- behavior ------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
private cycleScope(): void {
|
|
207
|
+
if (this.providers.length === 0) return;
|
|
208
|
+
if (this.scope === "all") {
|
|
209
|
+
this.scope = this.providers[0];
|
|
210
|
+
} else {
|
|
211
|
+
const idx = this.providers.indexOf(this.scope);
|
|
212
|
+
this.scope = idx >= 0 && idx < this.providers.length - 1 ? this.providers[idx + 1] : "all";
|
|
213
|
+
}
|
|
214
|
+
this.applyFilter(this.searchInput.getValue());
|
|
215
|
+
this.updateScopeText();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private activeRows(): Row[] {
|
|
219
|
+
if (this.scope === "all") return this.allRows;
|
|
220
|
+
return this.allRows.filter((r) => r.kind === "model" && r.provider === this.scope);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private applyFilter(query: string): void {
|
|
224
|
+
const active = this.activeRows();
|
|
225
|
+
this.filteredRows = query.trim()
|
|
226
|
+
? fuzzyFilter(active, query, rowSearchText)
|
|
227
|
+
: active;
|
|
228
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredRows.length - 1));
|
|
229
|
+
if (this.filteredRows.length === 0) this.selectedIndex = 0;
|
|
230
|
+
this.updateList();
|
|
231
|
+
this.tui.requestRender();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private moveSelection(delta: number): void {
|
|
235
|
+
if (this.filteredRows.length === 0) return;
|
|
236
|
+
const n = this.filteredRows.length;
|
|
237
|
+
// wrap for single-step, clamp for page jumps
|
|
238
|
+
if (Math.abs(delta) === 1) {
|
|
239
|
+
this.selectedIndex = (this.selectedIndex + delta + n) % n;
|
|
240
|
+
} else {
|
|
241
|
+
this.selectedIndex = Math.max(0, Math.min(n - 1, this.selectedIndex + delta));
|
|
242
|
+
}
|
|
243
|
+
this.updateList();
|
|
244
|
+
this.tui.requestRender();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private confirmSelection(): void {
|
|
248
|
+
const row = this.filteredRows[this.selectedIndex];
|
|
249
|
+
if (!row) return;
|
|
250
|
+
if (row.kind === "skip") {
|
|
251
|
+
this.done(MODEL_PICKER_SKIP);
|
|
252
|
+
} else {
|
|
253
|
+
this.done(row.label);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private updateHint(title: string): void {
|
|
258
|
+
const hints = [
|
|
259
|
+
this.theme.fg("muted", this.zh ? "输入关键词过滤(如 glm-5.2 或 zenmux glm)" : "type to filter (e.g. glm-5.2 or zenmux glm)"),
|
|
260
|
+
" " + this.theme.fg("muted", this.zh ? "Tab 切换 provider" : "Tab switch provider"),
|
|
261
|
+
" " + this.theme.fg("muted", this.zh ? "↑↓ 选择" : "↑↓ select"),
|
|
262
|
+
" " + this.theme.fg("muted", this.zh ? "回车确认 · Esc 取消" : "Enter confirm · Esc cancel"),
|
|
263
|
+
];
|
|
264
|
+
this.hint.setText(`${this.theme.bold(title)} ${hints.join(" ")}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private updateScopeText(): void {
|
|
268
|
+
const allLabel = this.zh ? "全部" : "all";
|
|
269
|
+
const parts = [this.scope === "all" ? this.theme.fg("accent", allLabel) : this.theme.fg("muted", allLabel)];
|
|
270
|
+
for (const p of this.providers) {
|
|
271
|
+
parts.push(p === this.scope ? this.theme.fg("accent", p) : this.theme.fg("muted", p));
|
|
272
|
+
}
|
|
273
|
+
const sep = this.theme.fg("muted", " | ");
|
|
274
|
+
this.scopeText.setText(this.theme.fg("muted", this.zh ? "Scope: " : "Scope: ") + parts.join(sep));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private updateList(): void {
|
|
278
|
+
this.listContainer.clear();
|
|
279
|
+
const rows = this.filteredRows;
|
|
280
|
+
const max = MAX_VISIBLE;
|
|
281
|
+
const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(max / 2), rows.length - max));
|
|
282
|
+
const end = Math.min(start + max, rows.length);
|
|
283
|
+
|
|
284
|
+
if (rows.length === 0) {
|
|
285
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", this.zh ? " 没有匹配的模型" : " No matching models"), 0, 0));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let i = start; i < end; i++) {
|
|
290
|
+
const row = rows[i];
|
|
291
|
+
const isSelected = i === this.selectedIndex;
|
|
292
|
+
const line = this.renderRow(row, isSelected);
|
|
293
|
+
this.listContainer.addChild(new Text(line, 0, 0));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (start > 0 || end < rows.length) {
|
|
297
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", ` (${this.selectedIndex + 1}/${rows.length})`), 0, 0));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private renderRow(row: Row, isSelected: boolean): string {
|
|
302
|
+
if (row.kind === "skip") {
|
|
303
|
+
const text = this.zh ? "跳过(用 Pi 默认)" : "Skip (Pi default)";
|
|
304
|
+
return isSelected
|
|
305
|
+
? this.theme.fg("accent", "→ ") + this.theme.fg("accent", text)
|
|
306
|
+
: ` ${this.theme.fg("muted", text)}`;
|
|
307
|
+
}
|
|
308
|
+
const prefix = isSelected ? this.theme.fg("accent", "→ ") : " ";
|
|
309
|
+
const modelText = isSelected ? this.theme.fg("accent", row.id) : this.theme.fg("text", row.id);
|
|
310
|
+
const badge = this.theme.fg("muted", `[${row.provider}]`);
|
|
311
|
+
return `${prefix}${modelText} ${badge}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_REVIEW_MODEL_PRIORITIES,
|
|
5
5
|
matchPresetEntry,
|
|
@@ -40,13 +40,20 @@ describe("resolveReviewProfile", () => {
|
|
|
40
40
|
|
|
41
41
|
describe("matchPresetEntry", () => {
|
|
42
42
|
const registry = [
|
|
43
|
-
m("openai", "gpt-5.
|
|
43
|
+
m("openai-codex", "gpt-5.6-sol"),
|
|
44
|
+
m("openai-codex", "gpt-5.6-terra"),
|
|
45
|
+
m("openai-codex", "gpt-5.6-luna"),
|
|
44
46
|
m("zhipu", "glm-5.2-chat"),
|
|
45
|
-
m("moonshot", "kimi-k2.7-
|
|
47
|
+
m("moonshot", "kimi-k2.7-code"),
|
|
46
48
|
m("moonshot", "kimi-k2.5"),
|
|
47
49
|
m("anthropic", "claude-opus-4-8"),
|
|
48
50
|
m("anthropic", "claude-opus-4-6"),
|
|
51
|
+
m("anthropic", "claude-sonnet-4-5"),
|
|
52
|
+
m("anthropic", "claude-fable-5"),
|
|
49
53
|
m("deepseek", "deepseek-v4-pro"),
|
|
54
|
+
m("deepseek", "deepseek-v4-flash"),
|
|
55
|
+
m("xai", "grok-4.5"),
|
|
56
|
+
m("minimax", "minimax-m3"),
|
|
50
57
|
];
|
|
51
58
|
|
|
52
59
|
it("picks newest kimi when versionPrefer 2.7", () => {
|
|
@@ -56,14 +63,27 @@ describe("matchPresetEntry", () => {
|
|
|
56
63
|
assert.match(hit!.id, /2\.7|k2\.7/i);
|
|
57
64
|
});
|
|
58
65
|
|
|
59
|
-
it("ranks
|
|
66
|
+
it("ranks fast-review models first for code presets", () => {
|
|
60
67
|
const ordered = rankModelsWithPresets(registry, "code", DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
61
|
-
assert.equal(ordered[0].id, "
|
|
62
|
-
assert.equal(ordered[1].id, "
|
|
68
|
+
assert.equal(ordered[0].id, "claude-sonnet-4-5");
|
|
69
|
+
assert.equal(ordered[1].id, "deepseek-v4-flash");
|
|
70
|
+
assert.ok(ordered.some((row) => row.id === "gpt-5.6-terra"));
|
|
71
|
+
assert.ok(ordered.some((row) => row.id === "gpt-5.6-luna"));
|
|
63
72
|
});
|
|
64
73
|
|
|
65
|
-
it("ranks
|
|
74
|
+
it("ranks complex/plan models with sol then opus then fable", () => {
|
|
66
75
|
const ordered = rankModelsWithPresets(registry, "plan", DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
67
|
-
assert.equal(ordered[0].id, "
|
|
76
|
+
assert.equal(ordered[0].id, "gpt-5.6-sol");
|
|
77
|
+
assert.equal(ordered[1].id, "claude-opus-4-8");
|
|
78
|
+
assert.equal(ordered[2].id, "claude-fable-5");
|
|
79
|
+
assert.ok(ordered.some((row) => row.id === "deepseek-v4-pro"));
|
|
80
|
+
assert.ok(ordered.some((row) => row.id === "grok-4.5"));
|
|
68
81
|
});
|
|
69
|
-
|
|
82
|
+
|
|
83
|
+
it("ranks vision/frontend with claude then gpt then kimi", () => {
|
|
84
|
+
const ordered = rankModelsWithPresets(registry, "frontend", DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
85
|
+
assert.match(ordered[0].id, /claude/i);
|
|
86
|
+
assert.ok(ordered.some((row) => /gpt/i.test(row.id)));
|
|
87
|
+
assert.ok(ordered.some((row) => /kimi/i.test(row.id) && /2\.7/i.test(row.id)));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -15,21 +15,36 @@ export type ReviewModelPriorities = {
|
|
|
15
15
|
plan: PresetModelEntry[];
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Default priorities when resources/rv-model-priorities.json is missing.
|
|
20
|
+
* - code: fast review
|
|
21
|
+
* - plan: complex / architecture / 方案评审 (max thinking)
|
|
22
|
+
* - frontend: vision / UI / multimodal
|
|
23
|
+
*/
|
|
18
24
|
export const DEFAULT_REVIEW_MODEL_PRIORITIES: ReviewModelPriorities = {
|
|
19
25
|
code: [
|
|
20
|
-
{ idContains: "
|
|
21
|
-
{ idContains: "
|
|
26
|
+
{ idContains: "claude-sonnet", versionPrefer: "5", thinking: "xhigh" },
|
|
27
|
+
{ idContains: "deepseek-v4-flash", thinking: "xhigh" },
|
|
28
|
+
{ idContains: "glm-5.2", thinking: "xhigh" },
|
|
29
|
+
{ idContains: "minimax-m3", thinking: "xhigh" },
|
|
30
|
+
{ idContains: "grok-4.5", thinking: "xhigh" },
|
|
31
|
+
{ idContains: "gpt-5.6-terra" },
|
|
32
|
+
{ idContains: "gpt-5.6-luna", thinking: "xhigh" },
|
|
22
33
|
],
|
|
23
34
|
frontend: [
|
|
35
|
+
{ idContains: "claude" },
|
|
36
|
+
{ idContains: "gpt" },
|
|
24
37
|
{ idContains: "kimi", versionPrefer: "2.7" },
|
|
25
|
-
{ idContains: "
|
|
26
|
-
{ idContains: "minimax-m3" },
|
|
38
|
+
{ idContains: "minimax-m3", thinking: "xhigh" },
|
|
27
39
|
],
|
|
28
40
|
plan: [
|
|
29
|
-
{ idContains: "
|
|
30
|
-
{ idContains: "claude-opus-4",
|
|
31
|
-
{ idContains: "
|
|
32
|
-
{ idContains: "
|
|
41
|
+
{ idContains: "gpt-5.6-sol", thinking: "xhigh" },
|
|
42
|
+
{ idContains: "claude-opus-4-8", thinking: "xhigh" },
|
|
43
|
+
{ idContains: "claude-opus-4", versionPrefer: "8", thinking: "xhigh" },
|
|
44
|
+
{ idContains: "claude-fable-5", thinking: "xhigh" },
|
|
45
|
+
{ idContains: "glm-5.2", thinking: "xhigh" },
|
|
46
|
+
{ idContains: "deepseek-v4-pro", thinking: "xhigh" },
|
|
47
|
+
{ idContains: "grok-4.5", thinking: "xhigh" },
|
|
33
48
|
],
|
|
34
49
|
};
|
|
35
50
|
|