@zephyrdeng/pi-review 0.5.1 → 0.6.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 +7 -1
- package/extensions/review.ts +60 -2
- package/extensions/rv-completions.test.ts +221 -0
- package/extensions/rv-completions.ts +450 -0
- package/extensions/rv-prompts.test.ts +15 -0
- package/extensions/rv-prompts.ts +19 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -222,7 +222,13 @@ The `/rv` command injects a task message for the parent agent (Pi host rules: de
|
|
|
222
222
|
/rv --mode challenge --keep-session @docs/design.md
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
Argument completions
|
|
225
|
+
Argument completions are context-aware. After the host session starts, `/rv` reads the live model registry and offers:
|
|
226
|
+
|
|
227
|
+
- **Model list** after `--model `, ranked by review fit (reasoning, context window, cross-vendor bonus for adversarial review) with a `★ 推荐` marker and a one-line reason.
|
|
228
|
+
- **Thinking suffix** after `provider/model:` — only levels the chosen model actually supports (e.g. `:high`, `:xhigh`); non-reasoning models offer none.
|
|
229
|
+
- **`--mode` / `--thinking` / flag** completions with hints, and **scene templates** (e.g. “审代码改动(推荐配置)”) at the top level that pre-fill a recommended command.
|
|
230
|
+
|
|
231
|
+
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.
|
|
226
232
|
|
|
227
233
|
## Security
|
|
228
234
|
|
package/extensions/review.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
buildRvCompletions,
|
|
4
|
+
type ModelInfo,
|
|
5
|
+
} from "./rv-completions.js";
|
|
2
6
|
import {
|
|
3
7
|
buildRvOrchestrationPrompt,
|
|
4
8
|
parseRvArgs,
|
|
@@ -6,11 +10,65 @@ import {
|
|
|
6
10
|
validateRvParsed,
|
|
7
11
|
} from "./rv-prompts.js";
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Captured at `session_start` so the synchronous-ish `getArgumentCompletions`
|
|
15
|
+
* callback (which receives only the argument prefix) can read the live model
|
|
16
|
+
* registry without ctx. Stays undefined in non-TUI/print mode; completions then
|
|
17
|
+
* gracefully degrade to the static `RV_COMPLETIONS` fallback.
|
|
18
|
+
*/
|
|
19
|
+
let capturedModels: ModelInfo[] | undefined;
|
|
20
|
+
let capturedPrimaryProvider: string | undefined;
|
|
21
|
+
|
|
22
|
+
function toModelInfo(m: {
|
|
23
|
+
provider: string;
|
|
24
|
+
id: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
reasoning?: boolean;
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
thinkingLevelMap?: Record<string, string | null | undefined>;
|
|
29
|
+
}): ModelInfo {
|
|
30
|
+
const levels: string[] = [];
|
|
31
|
+
const map = m.thinkingLevelMap ?? {};
|
|
32
|
+
for (const k of ["off", "minimal", "low", "medium", "high", "xhigh"]) {
|
|
33
|
+
// A level is supported unless explicitly disabled (null) and is present in the map.
|
|
34
|
+
if (k in map && map[k] !== null && map[k] !== undefined) levels.push(k);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
provider: m.provider,
|
|
38
|
+
id: m.id,
|
|
39
|
+
label: `${m.provider}/${m.id}`,
|
|
40
|
+
name: m.name ?? m.id,
|
|
41
|
+
reasoning: Boolean(m.reasoning),
|
|
42
|
+
contextWindow: m.contextWindow ?? 0,
|
|
43
|
+
thinkingLevels: levels,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
export default function piReviewExtension(pi: ExtensionAPI) {
|
|
48
|
+
pi.on("session_start", (_event, ctx) => {
|
|
49
|
+
try {
|
|
50
|
+
const available = ctx.modelRegistry?.getAvailable?.() ?? [];
|
|
51
|
+
capturedModels = available.map((m) => toModelInfo(m as unknown as Parameters<typeof toModelInfo>[0]));
|
|
52
|
+
capturedPrimaryProvider = ctx.model?.provider;
|
|
53
|
+
} catch {
|
|
54
|
+
capturedModels = undefined;
|
|
55
|
+
capturedPrimaryProvider = undefined;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
10
59
|
pi.registerCommand("rv", {
|
|
11
60
|
description:
|
|
12
|
-
"Delegate pi-review to the agent. Usage: /rv [--mode plan|challenge] [--model id] [--keep-session] [--no-stream] @target | /rv --continue <handle> [opts] [text] | models",
|
|
13
|
-
getArgumentCompletions: (prefix) => {
|
|
61
|
+
"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",
|
|
62
|
+
getArgumentCompletions: (prefix): ReturnType<typeof buildRvCompletions> => {
|
|
63
|
+
// Try dynamic completion first (models + thinking + scene templates).
|
|
64
|
+
if (capturedModels && capturedModels.length > 0) {
|
|
65
|
+
const dynamic = buildRvCompletions(prefix, {
|
|
66
|
+
models: capturedModels,
|
|
67
|
+
primaryProvider: capturedPrimaryProvider,
|
|
68
|
+
});
|
|
69
|
+
if (dynamic && dynamic.length) return dynamic;
|
|
70
|
+
}
|
|
71
|
+
// Graceful fallback to static hint list when registry is unavailable.
|
|
14
72
|
const filtered = RV_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
|
|
15
73
|
return filtered.length
|
|
16
74
|
? filtered.map(({ value, hint }) => ({ value, label: value, description: hint }))
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildRvCompletions,
|
|
5
|
+
extractSignals,
|
|
6
|
+
fuzzyMatch,
|
|
7
|
+
MODE_HINTS,
|
|
8
|
+
rankModelsForReview,
|
|
9
|
+
scoreModelForReview,
|
|
10
|
+
tokenizeArgPrefix,
|
|
11
|
+
type ModelInfo,
|
|
12
|
+
} from "./rv-completions.js";
|
|
13
|
+
|
|
14
|
+
function model(partial: Partial<ModelInfo> & Pick<ModelInfo, "provider" | "id">): ModelInfo {
|
|
15
|
+
return {
|
|
16
|
+
label: `${partial.provider}/${partial.id}`,
|
|
17
|
+
name: partial.id,
|
|
18
|
+
reasoning: false,
|
|
19
|
+
contextWindow: 200_000,
|
|
20
|
+
thinkingLevels: ["off", "low", "medium", "high"],
|
|
21
|
+
...partial,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MODELS: ModelInfo[] = [
|
|
26
|
+
model({ provider: "anthropic", id: "claude-opus-4-6", reasoning: true, contextWindow: 200_000, thinkingLevels: ["off", "low", "medium", "high", "xhigh"] }),
|
|
27
|
+
model({ provider: "openai", id: "gpt-5.5", reasoning: true, contextWindow: 400_000, thinkingLevels: ["off", "minimal", "low", "medium", "high", "xhigh"] }),
|
|
28
|
+
model({ provider: "zenmux", id: "gemini-3-flash", reasoning: false, contextWindow: 1_000_000, thinkingLevels: [] }),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
describe("tokenizeArgPrefix", () => {
|
|
32
|
+
it("splits head and tail on the last space", () => {
|
|
33
|
+
const t = tokenizeArgPrefix("@src/foo.ts --model ope");
|
|
34
|
+
assert.deepEqual(t.head, ["@src/foo.ts", "--model"]);
|
|
35
|
+
assert.equal(t.tail, "ope");
|
|
36
|
+
assert.equal(t.prev, "--model");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("treats a single token as tail with empty head", () => {
|
|
40
|
+
const t = tokenizeArgPrefix("--mode");
|
|
41
|
+
assert.deepEqual(t.head, []);
|
|
42
|
+
assert.equal(t.tail, "--mode");
|
|
43
|
+
assert.equal(t.prev, null);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles trailing space as empty tail", () => {
|
|
47
|
+
const t = tokenizeArgPrefix("@a.ts ");
|
|
48
|
+
assert.deepEqual(t.head, ["@a.ts"]);
|
|
49
|
+
assert.equal(t.tail, "");
|
|
50
|
+
assert.equal(t.prev, "@a.ts");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("strips surrounding quotes in head tokens", () => {
|
|
54
|
+
const t = tokenizeArgPrefix('"@a b.ts" --mode ');
|
|
55
|
+
assert.deepEqual(t.head, ["@a b.ts", "--mode"]);
|
|
56
|
+
assert.deepEqual(t.rawHead, ['"@a b.ts"', "--mode"]);
|
|
57
|
+
assert.equal(t.prev, "--mode");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("extractSignals", () => {
|
|
62
|
+
it("detects target extension and explicit mode", () => {
|
|
63
|
+
const sig = extractSignals(["@docs/design.md", "--mode", "plan"], {});
|
|
64
|
+
assert.equal(sig.mode, "plan");
|
|
65
|
+
assert.equal(sig.targetExt, "md");
|
|
66
|
+
assert.equal(sig.hasTarget, true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("defaults mode to code", () => {
|
|
70
|
+
const sig = extractSignals(["@src/foo.ts"], {});
|
|
71
|
+
assert.equal(sig.mode, "code");
|
|
72
|
+
assert.equal(sig.targetExt, "ts");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("carries primary provider for cross-vendor bonus", () => {
|
|
76
|
+
const sig = extractSignals([], { primaryProvider: "anthropic" });
|
|
77
|
+
assert.equal(sig.primaryProvider, "anthropic");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("scoreModelForReview / rankModelsForReview", () => {
|
|
82
|
+
it("scores reasoning + cross-vendor + xhigh higher than a cheap non-reasoning model", () => {
|
|
83
|
+
const sig = { mode: "challenge", hasTarget: true, targetExt: "md", primaryProvider: "anthropic" };
|
|
84
|
+
const opus = scoreModelForReview(MODELS[0], sig);
|
|
85
|
+
const gpt = scoreModelForReview(MODELS[1], sig);
|
|
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");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("ranks descending by score", () => {
|
|
93
|
+
const sig = { mode: "plan", hasTarget: false, primaryProvider: "anthropic" };
|
|
94
|
+
const ranked = rankModelsForReview(MODELS, sig);
|
|
95
|
+
assert.equal(ranked[0].id, "gpt-5.5");
|
|
96
|
+
assert.equal(ranked[ranked.length - 1].id, "gemini-3-flash");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("fuzzyMatch", () => {
|
|
101
|
+
it("matches subsequence case-insensitively", () => {
|
|
102
|
+
assert.equal(fuzzyMatch("anthropic/claude-opus-4-6", "aclaude"), true);
|
|
103
|
+
assert.equal(fuzzyMatch("Anthropic/Claude", "acl"), true);
|
|
104
|
+
assert.equal(fuzzyMatch("openai/gpt-5.5", "zzz"), false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("buildRvCompletions", () => {
|
|
109
|
+
const deps = { models: MODELS, primaryProvider: "anthropic" };
|
|
110
|
+
|
|
111
|
+
it("returns dynamic model list after --model, with recommended marker on top items", () => {
|
|
112
|
+
const items = buildRvCompletions("@x.md --model ", deps);
|
|
113
|
+
assert.ok(items, "expected items");
|
|
114
|
+
assert.ok(items!.some((i) => i.label === "openai/gpt-5.5"));
|
|
115
|
+
const top = items!.find((i) => i.description?.includes("★ 推荐"));
|
|
116
|
+
assert.ok(top, "expected at least one recommended item");
|
|
117
|
+
// value carries the head so applyCompletion replaces the whole arg safely
|
|
118
|
+
assert.ok(top!.value.startsWith("@x.md --model "));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("filters models by a partial query", () => {
|
|
122
|
+
const items = buildRvCompletions("--model ope", deps);
|
|
123
|
+
assert.ok(items);
|
|
124
|
+
assert.ok(items!.every((i) => i.label.includes("openai")));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("completes thinking suffix after provider/id: for a reasoning model", () => {
|
|
128
|
+
const items = buildRvCompletions("--model openai/gpt-5.5:", deps);
|
|
129
|
+
assert.ok(items);
|
|
130
|
+
const labels = items!.map((i) => i.label);
|
|
131
|
+
assert.ok(labels.includes("high"));
|
|
132
|
+
assert.ok(labels.includes("xhigh"));
|
|
133
|
+
assert.ok(items!.every((i) => i.value.startsWith("--model openai/gpt-5.5:")));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("does not offer thinking suffix for a non-reasoning model", () => {
|
|
137
|
+
const items = buildRvCompletions("--model zenmux/gemini-3-flash:", deps);
|
|
138
|
+
// flash has empty thinkingLevels → no suffix candidates
|
|
139
|
+
assert.equal(items, null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("completes --mode values with hints", () => {
|
|
143
|
+
const items = buildRvCompletions("--mode pl", deps);
|
|
144
|
+
assert.ok(items);
|
|
145
|
+
assert.deepEqual(items!.map((i) => i.label), ["plan"]);
|
|
146
|
+
assert.equal(items![0].description, MODE_HINTS["plan"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("completes --thinking levels", () => {
|
|
150
|
+
const items = buildRvCompletions("--thinking h", deps);
|
|
151
|
+
assert.ok(items);
|
|
152
|
+
assert.deepEqual(items!.map((i) => i.label).sort(), ["high"]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("completes flags when tail starts with --", () => {
|
|
156
|
+
const items = buildRvCompletions("--keep", deps);
|
|
157
|
+
assert.ok(items);
|
|
158
|
+
assert.deepEqual(items!.map((i) => i.label), ["--keep-session"]);
|
|
159
|
+
assert.ok(items![0].value === "--keep-session");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("defers to built-in file completion when tail starts with @", () => {
|
|
163
|
+
assert.equal(buildRvCompletions("@src/foo", deps), null);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("defers --continue value to the handler (no static list)", () => {
|
|
167
|
+
assert.equal(buildRvCompletions("--continue abc", deps), null);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("offers scene templates + models keyword at empty top level", () => {
|
|
171
|
+
const items = buildRvCompletions("", deps);
|
|
172
|
+
assert.ok(items);
|
|
173
|
+
assert.ok(items!.some((i) => i.label === "models"));
|
|
174
|
+
assert.ok(items!.some((i) => i.label.startsWith("审代码改动")));
|
|
175
|
+
// template values end with " @" so the user can keep typing a path
|
|
176
|
+
const tmpl = items!.find((i) => i.label.startsWith("审代码改动"))!;
|
|
177
|
+
assert.ok(tmpl.value.endsWith(" @"));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("every returned value preserves the head prefix (整段替换安全)", () => {
|
|
181
|
+
const cases = ["@a.ts --mode ", "@a.ts --model ", "@a.ts --thinking ", "@a.ts --"];
|
|
182
|
+
for (const c of cases) {
|
|
183
|
+
const items = buildRvCompletions(c, deps);
|
|
184
|
+
if (!items) continue;
|
|
185
|
+
for (const it of items) {
|
|
186
|
+
assert.ok(
|
|
187
|
+
it.value.startsWith(c),
|
|
188
|
+
`value "${it.value}" must preserve head "${c}"`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("preserves quotes in headPrefix for paths with spaces (note #2 fix)", () => {
|
|
195
|
+
const items = buildRvCompletions('"@a b.ts" --model ', deps);
|
|
196
|
+
assert.ok(items);
|
|
197
|
+
for (const it of items) {
|
|
198
|
+
assert.ok(
|
|
199
|
+
it.value.startsWith('"@a b.ts" --model '),
|
|
200
|
+
`value "${it.value}" must preserve quoted head`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("scene templates omit the thinking colon for non-reasoning models (note #1 fix)", () => {
|
|
206
|
+
const nonReasoning = [{ ...MODELS[2], thinkingLevels: [] }] as ModelInfo[];
|
|
207
|
+
const items = buildRvCompletions("", { models: nonReasoning });
|
|
208
|
+
assert.ok(items);
|
|
209
|
+
const tmpl = items!.find((i) => i.label.startsWith("审代码改动"));
|
|
210
|
+
assert.ok(tmpl, "expected a scene template even for non-reasoning models");
|
|
211
|
+
// Must NOT contain a dangling colon like `gemini-3-flash: @`
|
|
212
|
+
assert.doesNotMatch(tmpl!.value, /flash:\s@/);
|
|
213
|
+
assert.match(tmpl!.value, /flash @/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("falls back gracefully when models are unavailable (static flags still work)", () => {
|
|
217
|
+
const items = buildRvCompletions("--mode ", {});
|
|
218
|
+
assert.ok(items);
|
|
219
|
+
assert.ok(items!.some((i) => i.label === "code" || i.label === "plan" || i.label === "challenge"));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, testable /rv argument-completion logic.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately avoids importing pi types so the tokenizer, state
|
|
5
|
+
* machine, and model scorer can be unit-tested with `node:test` and reused
|
|
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`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type AutocompleteItem = {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const RV_MODE_VALUES = ["code", "plan", "challenge"] as const;
|
|
21
|
+
export const THINKING_LEVELS = [
|
|
22
|
+
"off",
|
|
23
|
+
"minimal",
|
|
24
|
+
"low",
|
|
25
|
+
"medium",
|
|
26
|
+
"high",
|
|
27
|
+
"xhigh",
|
|
28
|
+
] as const;
|
|
29
|
+
export const RV_FLAGS = [
|
|
30
|
+
"--mode",
|
|
31
|
+
"--model",
|
|
32
|
+
"--thinking",
|
|
33
|
+
"--keep-session",
|
|
34
|
+
"--no-stream",
|
|
35
|
+
"--continue",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export const MODE_HINTS: Record<string, string> = {
|
|
39
|
+
code: "代码 / diff / MR 审核",
|
|
40
|
+
plan: "架构 / 计划评审",
|
|
41
|
+
challenge: "对抗性方案评审",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const FLAG_HINTS: Record<string, string> = {
|
|
45
|
+
"--mode": "审核模式 code|plan|challenge",
|
|
46
|
+
"--model": "provider/model[:thinking]",
|
|
47
|
+
"--thinking": "off|minimal|low|medium|high|xhigh",
|
|
48
|
+
"--keep-session": "保留会话,可用 /rv --continue 追问",
|
|
49
|
+
"--no-stream": "缓冲输出到结束(非默认)",
|
|
50
|
+
"--continue": "继续既有 review 会话",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Minimal model metadata we need; converted from the host's Model registry. */
|
|
54
|
+
export interface ModelInfo {
|
|
55
|
+
provider: string;
|
|
56
|
+
id: string;
|
|
57
|
+
/** `${provider}/${id}` */
|
|
58
|
+
label: string;
|
|
59
|
+
name: string;
|
|
60
|
+
reasoning: boolean;
|
|
61
|
+
contextWindow: number;
|
|
62
|
+
/** Lowercased thinking levels the model actually supports; empty if non-reasoning. */
|
|
63
|
+
thinkingLevels: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ReviewSignals {
|
|
67
|
+
mode: string;
|
|
68
|
+
hasTarget: boolean;
|
|
69
|
+
targetExt?: string;
|
|
70
|
+
/** Provider of the host session's primary model, for cross-vendor bonus. */
|
|
71
|
+
primaryProvider?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CompletionDeps {
|
|
75
|
+
models?: ModelInfo[];
|
|
76
|
+
primaryProvider?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Tokenizer
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export interface TokenizedArgs {
|
|
84
|
+
/** Fully typed tokens before the in-progress tail. */
|
|
85
|
+
head: string[];
|
|
86
|
+
/** Raw head tokens (quotes preserved) for safe reconstruction. */
|
|
87
|
+
rawHead: string[];
|
|
88
|
+
/** The token currently being typed (may be ""). */
|
|
89
|
+
tail: string;
|
|
90
|
+
/** Last token of `head`, or null. Used to detect value-position flags. */
|
|
91
|
+
prev: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const TOKEN_RE = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;
|
|
95
|
+
|
|
96
|
+
export function tokenizeArgPrefix(prefix: string): TokenizedArgs {
|
|
97
|
+
const lastSpace = prefix.lastIndexOf(" ");
|
|
98
|
+
if (lastSpace === -1) {
|
|
99
|
+
return { head: [], rawHead: [], tail: prefix, prev: null };
|
|
100
|
+
}
|
|
101
|
+
const headRaw = prefix.slice(0, lastSpace);
|
|
102
|
+
const tail = prefix.slice(lastSpace + 1);
|
|
103
|
+
const matches = headRaw.match(TOKEN_RE) ?? [];
|
|
104
|
+
const rawHead = matches.slice();
|
|
105
|
+
const head = matches.map((t) => t.replace(/^['"]|['"]$/g, ""));
|
|
106
|
+
const prev = head.length > 0 ? head[head.length - 1] : null;
|
|
107
|
+
return { head, rawHead, tail, prev };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Signal extraction
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
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
|
+
|
|
121
|
+
function extOf(token: string): string | undefined {
|
|
122
|
+
const cleaned = token.replace(/^['"]|['"]$/g, "").replace(/^@/, "");
|
|
123
|
+
const m = cleaned.match(/\.([a-zA-Z0-9]+)$/);
|
|
124
|
+
return m ? m[1].toLowerCase() : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function extractSignals(head: string[], deps: CompletionDeps): ReviewSignals {
|
|
128
|
+
const target = head.find((t) => t.startsWith("@"));
|
|
129
|
+
const modeIdx = head.indexOf("--mode");
|
|
130
|
+
const mode = modeIdx >= 0 ? head[modeIdx + 1] : "code";
|
|
131
|
+
return {
|
|
132
|
+
mode,
|
|
133
|
+
hasTarget: Boolean(target),
|
|
134
|
+
targetExt: target ? extOf(target) : undefined,
|
|
135
|
+
primaryProvider: deps.primaryProvider,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Model scoring (P2 rule engine — pure, no LLM in the completion hot path)
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
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
|
+
export function rankModelsForReview(
|
|
173
|
+
models: ModelInfo[],
|
|
174
|
+
sig: ReviewSignals,
|
|
175
|
+
): ModelInfo[] {
|
|
176
|
+
return models
|
|
177
|
+
.map((m) => ({ m, s: scoreModelForReview(m, sig).score }))
|
|
178
|
+
.sort((a, b) => b.s - a.s)
|
|
179
|
+
.map((x) => x.m);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Fuzzy filtering (subsequence match, case-insensitive)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
export function fuzzyMatch(text: string, query: string): boolean {
|
|
187
|
+
if (!query) return true;
|
|
188
|
+
const t = text.toLowerCase();
|
|
189
|
+
const q = query.toLowerCase();
|
|
190
|
+
let i = 0;
|
|
191
|
+
for (const ch of t) {
|
|
192
|
+
if (ch === q[i]) i++;
|
|
193
|
+
if (i === q.length) return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function filterList(list: readonly string[], query: string): string[] {
|
|
199
|
+
if (!query) return [...list];
|
|
200
|
+
const prefixed = list.filter((item) => item.startsWith(query));
|
|
201
|
+
if (prefixed.length) return prefixed;
|
|
202
|
+
// Only fall back to subsequence matching when nothing starts with the query.
|
|
203
|
+
return list.filter((item) => fuzzyMatch(item, query));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function uniqueFlat(lists: string[][]): string[] {
|
|
207
|
+
const seen = new Set<string>();
|
|
208
|
+
const out: string[] = [];
|
|
209
|
+
for (const list of lists) {
|
|
210
|
+
for (const v of list) {
|
|
211
|
+
if (!seen.has(v)) {
|
|
212
|
+
seen.add(v);
|
|
213
|
+
out.push(v);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Completion builders
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function wrap(
|
|
225
|
+
candidates: string[],
|
|
226
|
+
headPrefix: string,
|
|
227
|
+
hints?: Record<string, string>,
|
|
228
|
+
): AutocompleteItem[] | null {
|
|
229
|
+
if (candidates.length === 0) return null;
|
|
230
|
+
return candidates.map((c) => ({
|
|
231
|
+
value: `${headPrefix}${c}`,
|
|
232
|
+
label: c,
|
|
233
|
+
description: hints?.[c],
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function thinkingDescription(level: string, sig: ReviewSignals): string {
|
|
238
|
+
const recommended = sig.mode === "code" ? "high" : "xhigh";
|
|
239
|
+
const star = level === recommended ? "★ 推荐 " : "";
|
|
240
|
+
const map: Record<string, string> = {
|
|
241
|
+
off: "不思考(最快)",
|
|
242
|
+
minimal: "极简思考",
|
|
243
|
+
low: "低强度",
|
|
244
|
+
medium: "中强度",
|
|
245
|
+
high: "高强度(review 默认)",
|
|
246
|
+
xhigh: "极致思考(plan/challenge 推荐)",
|
|
247
|
+
};
|
|
248
|
+
return `${star}${map[level] ?? level}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function contextLabel(ctx: number): string {
|
|
252
|
+
if (ctx >= 1_000_000) return "1M+ ctx";
|
|
253
|
+
if (ctx >= 1000) return `${Math.round(ctx / 1000)}k ctx`;
|
|
254
|
+
return `${ctx} ctx`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function modelItems(
|
|
258
|
+
models: ModelInfo[],
|
|
259
|
+
sig: ReviewSignals,
|
|
260
|
+
headPrefix: string,
|
|
261
|
+
tail: string,
|
|
262
|
+
): AutocompleteItem[] | null {
|
|
263
|
+
const ranked = rankModelsForReview(models, sig);
|
|
264
|
+
const items = ranked.map((m, i) => {
|
|
265
|
+
const { score, reason } = scoreModelForReview(m, sig);
|
|
266
|
+
const recommended = i < 3 && score > 0;
|
|
267
|
+
const tag =
|
|
268
|
+
sig.mode === "plan" || sig.mode === "challenge" ? "plan/challenge" : "review";
|
|
269
|
+
return {
|
|
270
|
+
value: `${headPrefix}${m.label}`,
|
|
271
|
+
label: m.label,
|
|
272
|
+
description: [
|
|
273
|
+
recommended ? "★ 推荐" : "",
|
|
274
|
+
tag,
|
|
275
|
+
m.reasoning ? "reasoning" : "",
|
|
276
|
+
contextLabel(m.contextWindow),
|
|
277
|
+
reason,
|
|
278
|
+
]
|
|
279
|
+
.filter(Boolean)
|
|
280
|
+
.join(" · "),
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
// Contiguous matching for model ids (subsequence is too loose for ids like
|
|
284
|
+
// "claude-opus" which would match "ope" via o-p...-e).
|
|
285
|
+
const q = tail;
|
|
286
|
+
const filtered = items.filter((it) => {
|
|
287
|
+
const label = it.label;
|
|
288
|
+
return (
|
|
289
|
+
label.startsWith(q) ||
|
|
290
|
+
label.includes(q) ||
|
|
291
|
+
// also allow matching by bare id / provider segment
|
|
292
|
+
label.split("/")[1]?.startsWith(q) ||
|
|
293
|
+
label.split("/")[0]?.startsWith(q)
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
return filtered.length ? filtered : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function thinkingSuffixItems(
|
|
300
|
+
tail: string,
|
|
301
|
+
models: ModelInfo[],
|
|
302
|
+
headPrefix: string,
|
|
303
|
+
sig: ReviewSignals,
|
|
304
|
+
): AutocompleteItem[] | null {
|
|
305
|
+
const colonIdx = tail.lastIndexOf(":");
|
|
306
|
+
const modelPart = tail.slice(0, colonIdx);
|
|
307
|
+
const levelPart = tail.slice(colonIdx + 1);
|
|
308
|
+
if (!modelPart.includes("/")) return null;
|
|
309
|
+
|
|
310
|
+
const exact = models.filter((m) => m.label === modelPart);
|
|
311
|
+
const pool = exact.length
|
|
312
|
+
? exact
|
|
313
|
+
: models.filter((m) => m.label.startsWith(modelPart) || m.id.startsWith(modelPart));
|
|
314
|
+
if (pool.length === 0) return null;
|
|
315
|
+
|
|
316
|
+
const levels = uniqueFlat(pool.map((m) => m.thinkingLevels));
|
|
317
|
+
const filtered = filterList(levels, levelPart);
|
|
318
|
+
if (filtered.length === 0) return null;
|
|
319
|
+
return filtered.map((level) => ({
|
|
320
|
+
value: `${headPrefix}${modelPart}:${level}`,
|
|
321
|
+
label: level,
|
|
322
|
+
description: thinkingDescription(level, sig),
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function sceneTemplates(
|
|
327
|
+
top: ModelInfo | undefined,
|
|
328
|
+
headPrefix: string,
|
|
329
|
+
tail: string,
|
|
330
|
+
): AutocompleteItem[] {
|
|
331
|
+
if (!top) return [];
|
|
332
|
+
const pref = (m: ModelInfo, want: string): string => {
|
|
333
|
+
if (m.thinkingLevels.includes(want)) return want;
|
|
334
|
+
if (m.thinkingLevels.includes("high")) return "high";
|
|
335
|
+
return m.thinkingLevels[0] ?? "";
|
|
336
|
+
};
|
|
337
|
+
const suffix = (want: string): string => {
|
|
338
|
+
const lvl = pref(top, want);
|
|
339
|
+
return lvl ? `:${lvl}` : "";
|
|
340
|
+
};
|
|
341
|
+
|
|
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
|
+
return templates
|
|
357
|
+
.filter((t) => fuzzyMatch(t.label, tail) || t.cmd.includes(tail))
|
|
358
|
+
.map((t) => ({
|
|
359
|
+
value: `${headPrefix}${t.cmd}`,
|
|
360
|
+
label: t.label,
|
|
361
|
+
description: t.cmd,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function flagItems(head: string[], tail: string, headPrefix: string): AutocompleteItem[] | null {
|
|
366
|
+
// Don't re-suggest boolean flags already present.
|
|
367
|
+
const present = new Set(head);
|
|
368
|
+
const candidates = filterList(RV_FLAGS, tail).filter((f) => {
|
|
369
|
+
if (f === "--keep-session" || f === "--no-stream") return !present.has(f);
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
return wrap(candidates, headPrefix, FLAG_HINTS);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function topLevelCompletions(
|
|
376
|
+
head: string[],
|
|
377
|
+
tail: string,
|
|
378
|
+
deps: CompletionDeps,
|
|
379
|
+
headPrefix: string,
|
|
380
|
+
sig: ReviewSignals,
|
|
381
|
+
): AutocompleteItem[] | null {
|
|
382
|
+
const items: AutocompleteItem[] = [];
|
|
383
|
+
|
|
384
|
+
// Scene templates (only at top level, before any target/mode chosen is fine).
|
|
385
|
+
const ranked = deps.models?.length ? rankModelsForReview(deps.models, sig) : [];
|
|
386
|
+
items.push(...sceneTemplates(ranked[0], headPrefix, tail));
|
|
387
|
+
|
|
388
|
+
// `models` keyword.
|
|
389
|
+
if (fuzzyMatch("models", tail) || "models".startsWith(tail)) {
|
|
390
|
+
items.push({
|
|
391
|
+
value: `${headPrefix}models`,
|
|
392
|
+
label: "models",
|
|
393
|
+
description: "仅列出 pi-review 可用模型",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Flags.
|
|
398
|
+
const flags = flagItems(head, tail, headPrefix);
|
|
399
|
+
if (flags) items.push(...flags);
|
|
400
|
+
|
|
401
|
+
return items.length ? items : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// Public entry point
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
export function buildRvCompletions(
|
|
409
|
+
prefix: string,
|
|
410
|
+
deps: CompletionDeps,
|
|
411
|
+
): AutocompleteItem[] | null {
|
|
412
|
+
const { head, rawHead, tail, prev } = tokenizeArgPrefix(prefix);
|
|
413
|
+
const joinedHead = rawHead.join(" ");
|
|
414
|
+
const headPrefix = joinedHead ? `${joinedHead} ` : "";
|
|
415
|
+
const sig = extractSignals(head, deps);
|
|
416
|
+
|
|
417
|
+
// 1) --model value position (model list or :thinking suffix).
|
|
418
|
+
if (prev === "--model") {
|
|
419
|
+
const colonIdx = tail.lastIndexOf(":");
|
|
420
|
+
if (colonIdx !== -1 && tail.slice(0, colonIdx).includes("/")) {
|
|
421
|
+
const items = thinkingSuffixItems(tail, deps.models ?? [], headPrefix, sig);
|
|
422
|
+
return items;
|
|
423
|
+
}
|
|
424
|
+
return modelItems(deps.models ?? [], sig, headPrefix, tail);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 2) --mode value position.
|
|
428
|
+
if (prev === "--mode") {
|
|
429
|
+
return wrap(filterList(RV_MODE_VALUES, tail), headPrefix, MODE_HINTS);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 3) --thinking value position.
|
|
433
|
+
if (prev === "--thinking") {
|
|
434
|
+
return wrap(filterList(THINKING_LEVELS, tail), headPrefix);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 4) --continue value position: defer (no static list; handler owns it).
|
|
438
|
+
if (prev === "--continue") return null;
|
|
439
|
+
|
|
440
|
+
// 5) Flag completion (tail starts with "--").
|
|
441
|
+
if (tail.startsWith("--")) {
|
|
442
|
+
return flagItems(head, tail, headPrefix);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 6) File attachment in progress → defer to built-in file completion.
|
|
446
|
+
if (tail.startsWith("@")) return null;
|
|
447
|
+
|
|
448
|
+
// 7) Top-level: scene templates + `models` + flags.
|
|
449
|
+
return topLevelCompletions(head, tail, deps, headPrefix, sig);
|
|
450
|
+
}
|
|
@@ -42,6 +42,21 @@ describe("parseRvArgs / validateRvParsed", () => {
|
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it("parses --thinking and threads it into CLI argv", () => {
|
|
46
|
+
const p = parseRvArgs("--thinking xhigh --model openai/gpt-5.5 @x.md");
|
|
47
|
+
assert.equal(p.thinking, "xhigh");
|
|
48
|
+
assert.equal(p.model, "openai/gpt-5.5");
|
|
49
|
+
assert.deepEqual(buildPiReviewArgv(p, "@x.md"), [
|
|
50
|
+
"pi-review",
|
|
51
|
+
"--model",
|
|
52
|
+
"openai/gpt-5.5",
|
|
53
|
+
"--thinking",
|
|
54
|
+
"xhigh",
|
|
55
|
+
"--",
|
|
56
|
+
"@x.md",
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
45
60
|
it("orchestration prompt forbids default no-stream and progress-log in Pi", () => {
|
|
46
61
|
const p = parseRvArgs("@src/foo.ts");
|
|
47
62
|
const prompt = buildRvOrchestrationPrompt(p);
|
package/extensions/rv-prompts.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
export const RV_MODES = ["code", "plan", "challenge"] as const;
|
|
4
4
|
export type RvMode = (typeof RV_MODES)[number];
|
|
5
5
|
|
|
6
|
-
const FLAGS_REQUIRING_VALUE = ["--mode", "--model", "--continue"] as const;
|
|
6
|
+
const FLAGS_REQUIRING_VALUE = ["--mode", "--model", "--thinking", "--continue"] as const;
|
|
7
7
|
|
|
8
8
|
export type RvParsed = {
|
|
9
9
|
mode: string;
|
|
10
10
|
model?: string;
|
|
11
|
+
thinking?: string;
|
|
11
12
|
keepSession: boolean;
|
|
12
13
|
noStream: boolean;
|
|
13
14
|
continueHandle?: string;
|
|
@@ -25,6 +26,7 @@ export function parseRvArgs(raw: string): RvParsed {
|
|
|
25
26
|
|
|
26
27
|
let mode = "code";
|
|
27
28
|
let model: string | undefined;
|
|
29
|
+
let thinking: string | undefined;
|
|
28
30
|
let keepSession = false;
|
|
29
31
|
let noStream = false;
|
|
30
32
|
let continueHandle: string | undefined;
|
|
@@ -63,6 +65,15 @@ export function parseRvArgs(raw: string): RvParsed {
|
|
|
63
65
|
model = cleaned[++i];
|
|
64
66
|
continue;
|
|
65
67
|
}
|
|
68
|
+
if (t === "--thinking") {
|
|
69
|
+
const v = cleaned[i + 1];
|
|
70
|
+
if (!v || v.startsWith("--")) {
|
|
71
|
+
rest.push(t);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
thinking = cleaned[++i];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
66
77
|
if (t === "--continue") {
|
|
67
78
|
const v = cleaned[i + 1];
|
|
68
79
|
if (!v || v.startsWith("--")) {
|
|
@@ -78,6 +89,7 @@ export function parseRvArgs(raw: string): RvParsed {
|
|
|
78
89
|
return {
|
|
79
90
|
mode,
|
|
80
91
|
model,
|
|
92
|
+
thinking,
|
|
81
93
|
keepSession,
|
|
82
94
|
noStream,
|
|
83
95
|
continueHandle,
|
|
@@ -108,6 +120,7 @@ export function buildPiReviewArgv(parsed: RvParsed, target: string): string[] {
|
|
|
108
120
|
}
|
|
109
121
|
if (parsed.mode !== "code") parts.push("--mode", parsed.mode);
|
|
110
122
|
if (parsed.model) parts.push("--model", parsed.model);
|
|
123
|
+
if (parsed.thinking) parts.push("--thinking", parsed.thinking);
|
|
111
124
|
if (parsed.noStream) parts.push("--no-stream");
|
|
112
125
|
parts.push("--", target);
|
|
113
126
|
return parts;
|
|
@@ -168,8 +181,10 @@ export function buildRvOrchestrationPrompt(parsed: RvParsed): string {
|
|
|
168
181
|
const cliLine = buildPiReviewArgv(parsed, target).join(" ");
|
|
169
182
|
|
|
170
183
|
const modelStep = parsed.model
|
|
171
|
-
? `Use model exactly as given: ${parsed.model} (must appear in pi-review models output).`
|
|
172
|
-
:
|
|
184
|
+
? `Use model exactly as given: ${parsed.model}${parsed.thinking ? ` at thinking ${parsed.thinking}` : ""} (model must appear in pi-review models output).`
|
|
185
|
+
: parsed.thinking
|
|
186
|
+
? `Run pi-review models first if needed; pick a suitable listed ID or omit --model for Pi default. Use thinking ${parsed.thinking}.`
|
|
187
|
+
: "Run pi-review models first if needed; pick a suitable listed ID or omit --model for Pi default.";
|
|
173
188
|
|
|
174
189
|
const continueNote = parsed.continueHandle
|
|
175
190
|
? "Continuing an existing review session. Optional --mode and --model match the same flags as an initial /rv run."
|
|
@@ -197,6 +212,7 @@ export const RV_COMPLETIONS: { value: string; hint: string }[] = [
|
|
|
197
212
|
{ value: "--mode plan", hint: "architecture or plan review" },
|
|
198
213
|
{ value: "--mode challenge", hint: "adversarial plan review" },
|
|
199
214
|
{ value: "--model ", hint: "provider/model from pi-review models" },
|
|
215
|
+
{ value: "--thinking ", hint: "off|minimal|low|medium|high|xhigh" },
|
|
200
216
|
{ value: "--keep-session", hint: "persist session for follow-up" },
|
|
201
217
|
{ value: "--no-stream", hint: "rare: buffer until exit (not default in Pi)" },
|
|
202
218
|
{ value: "--continue ", hint: "resume session; same optional flags as initial /rv" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zephyrdeng/pi-review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Isolated AI-powered code and plan reviews from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "ZephyrDeng <zephyrTang@aliyun.com>",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"build": "tsc",
|
|
68
68
|
"check": "tsc --noEmit",
|
|
69
69
|
"smoke": "node dist/cli.js --help >/dev/null",
|
|
70
|
-
"test": "npx --yes tsx --test src/child-process.test.ts src/review.meta.test.ts src/meta-footer.test.ts src/json-events.test.ts src/install-args.test.ts extensions/rv-prompts.test.ts",
|
|
71
|
-
"test:rv": "npx --yes tsx --test extensions/rv-prompts.test.ts",
|
|
70
|
+
"test": "npx --yes tsx --test src/child-process.test.ts src/review.meta.test.ts src/meta-footer.test.ts src/json-events.test.ts src/install-args.test.ts extensions/rv-prompts.test.ts extensions/rv-completions.test.ts",
|
|
71
|
+
"test:rv": "npx --yes tsx --test extensions/rv-prompts.test.ts extensions/rv-completions.test.ts",
|
|
72
72
|
"prepublishOnly": "npm run build",
|
|
73
73
|
"pack:dry": "npm pack --dry-run"
|
|
74
74
|
},
|