@zhushanwen/pi-permission 1.3.4 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -20
- package/package.json +4 -3
- package/skills/permission-ext-config/SKILL.md +10 -9
- package/src/__tests__/approval.test.ts +1 -2
- package/src/__tests__/index-integration.test.ts +5 -4
- package/src/__tests__/pipeline.test.ts +76 -0
- package/src/__tests__/production.test.ts +4 -4
- package/src/__tests__/rule-templates.test.ts +0 -34
- package/src/__tests__/session-start-once-guard.test.ts +147 -0
- package/src/approval.ts +11 -9
- package/src/ast/analyzer.ts +78 -68
- package/src/classifier/__tests__/classifier.test.ts +1 -1
- package/src/classifier/classifier.ts +82 -35
- package/src/classifier/model-resolver.ts +0 -3
- package/src/index.ts +9 -10
- package/src/model-picker.ts +5 -18
- package/src/pipeline.ts +275 -166
- package/src/production.ts +2 -2
- package/src/rule-editor-component.ts +4 -13
- package/src/rule-editor.ts +78 -29
- package/src/rule-templates.ts +1 -52
- package/src/rules/matcher.ts +1 -1
- package/src/select-theme.ts +15 -0
- package/src/statusline-palette.ts +2 -1
- package/src/types.ts +16 -27
package/src/ast/analyzer.ts
CHANGED
|
@@ -102,90 +102,100 @@ function parseRawString(node: Node): string | null {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* 5 分支处理 named children:
|
|
108
|
-
* 1. command_name → 其第一个 named child 必须是 word,取文本
|
|
109
|
-
* 2. word | number → 直接取文本
|
|
110
|
-
* 3. string → parseDoubleQuotedString(含 expansion 则 null)
|
|
111
|
-
* 4. raw_string → parseRawString
|
|
112
|
-
* 5. concatenation(如 -g"*.py")→ 递归拼接各 part(word/number/string/raw_string)
|
|
113
|
-
*
|
|
114
|
-
* 任一分支失败 → 返回 null(该 command 被跳过,但 clean 已在 DFS 阶段被置 false)。
|
|
105
|
+
* command_name 分支:其第一个 named child 必须是 word,取文本;否则 null。
|
|
115
106
|
*/
|
|
116
|
-
function
|
|
117
|
-
|
|
107
|
+
function parseCommandName(child: Node): string | null {
|
|
108
|
+
const wordNode = child.namedChild(0);
|
|
109
|
+
if (!wordNode || wordNode.type !== "word") {
|
|
118
110
|
return null;
|
|
119
111
|
}
|
|
112
|
+
return wordNode.text;
|
|
113
|
+
}
|
|
120
114
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
words.push(wordNode.text);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
115
|
+
/**
|
|
116
|
+
* concatenation 分支(如 -g"*.py" 或 '/usr'"/"'local'/bin):
|
|
117
|
+
* 递归拼接各 part(word/number/string/raw_string),空拼接结果 → null。
|
|
118
|
+
*/
|
|
119
|
+
function parseConcatenation(child: Node): string | null {
|
|
120
|
+
let concatenated = "";
|
|
121
|
+
for (const part of child.namedChildren) {
|
|
122
|
+
switch (part.type) {
|
|
132
123
|
case "word":
|
|
133
|
-
case "number":
|
|
134
|
-
|
|
124
|
+
case "number":
|
|
125
|
+
concatenated += part.text;
|
|
135
126
|
break;
|
|
136
|
-
}
|
|
137
127
|
case "string": {
|
|
138
|
-
const parsed = parseDoubleQuotedString(
|
|
139
|
-
if (parsed === null)
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
words.push(parsed);
|
|
128
|
+
const parsed = parseDoubleQuotedString(part);
|
|
129
|
+
if (parsed === null) return null;
|
|
130
|
+
concatenated += parsed;
|
|
143
131
|
break;
|
|
144
132
|
}
|
|
145
133
|
case "raw_string": {
|
|
146
|
-
const parsed = parseRawString(
|
|
147
|
-
if (parsed === null)
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
words.push(parsed);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case "concatenation": {
|
|
154
|
-
// 拼接如 -g"*.py" 或 '/usr'"/"'local'/bin
|
|
155
|
-
let concatenated = "";
|
|
156
|
-
for (const part of child.namedChildren) {
|
|
157
|
-
switch (part.type) {
|
|
158
|
-
case "word":
|
|
159
|
-
case "number":
|
|
160
|
-
concatenated += part.text;
|
|
161
|
-
break;
|
|
162
|
-
case "string": {
|
|
163
|
-
const parsed = parseDoubleQuotedString(part);
|
|
164
|
-
if (parsed === null) return null;
|
|
165
|
-
concatenated += parsed;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case "raw_string": {
|
|
169
|
-
const parsed = parseRawString(part);
|
|
170
|
-
if (parsed === null) return null;
|
|
171
|
-
concatenated += parsed;
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
default:
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (concatenated.length === 0) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
words.push(concatenated);
|
|
134
|
+
const parsed = parseRawString(part);
|
|
135
|
+
if (parsed === null) return null;
|
|
136
|
+
concatenated += parsed;
|
|
182
137
|
break;
|
|
183
138
|
}
|
|
184
139
|
default:
|
|
185
|
-
// 未知 named child(不应发生 —— DFS 阶段已拒,但保险起见 fail-closed)
|
|
186
140
|
return null;
|
|
187
141
|
}
|
|
188
142
|
}
|
|
143
|
+
if (concatenated.length === 0) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return concatenated;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 解析 command 的单个 named child 为一个 argv token(分发表)。
|
|
151
|
+
*
|
|
152
|
+
* 5 分支:
|
|
153
|
+
* 1. command_name → parseCommandName(第一个 named child 必须是 word)
|
|
154
|
+
* 2. word | number → 直接取文本
|
|
155
|
+
* 3. string → parseDoubleQuotedString(含 expansion 则 null)
|
|
156
|
+
* 4. raw_string → parseRawString
|
|
157
|
+
* 5. concatenation → parseConcatenation
|
|
158
|
+
*
|
|
159
|
+
* 未知 named child(不应发生 —— DFS 阶段已拒,但保险起见 fail-closed)→ null。
|
|
160
|
+
* 失败 → 调用方让整个 command 返回 null(该 command 被跳过,但 clean 已在 DFS 阶段被置 false)。
|
|
161
|
+
*/
|
|
162
|
+
function parseChildToken(child: Node): string | null {
|
|
163
|
+
switch (child.type) {
|
|
164
|
+
case "command_name":
|
|
165
|
+
return parseCommandName(child);
|
|
166
|
+
case "word":
|
|
167
|
+
case "number":
|
|
168
|
+
return child.text;
|
|
169
|
+
case "string":
|
|
170
|
+
return parseDoubleQuotedString(child);
|
|
171
|
+
case "raw_string":
|
|
172
|
+
return parseRawString(child);
|
|
173
|
+
case "concatenation":
|
|
174
|
+
return parseConcatenation(child);
|
|
175
|
+
default:
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 从一个 command 节点提取 argv(对应 Rust parse_plain_command_from_node)。
|
|
182
|
+
*
|
|
183
|
+
* 主函数编排:类型守卫 → 逐 named child 分发(parseChildToken)→ 组装 words。
|
|
184
|
+
* 任一 child 解析失败 → 返回 null。
|
|
185
|
+
*/
|
|
186
|
+
function parsePlainCommand(cmd: Node): string[] | null {
|
|
187
|
+
if (cmd.type !== "command") {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const words: string[] = [];
|
|
192
|
+
for (const child of cmd.namedChildren) {
|
|
193
|
+
const token = parseChildToken(child);
|
|
194
|
+
if (token === null) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
words.push(token);
|
|
198
|
+
}
|
|
189
199
|
return words;
|
|
190
200
|
}
|
|
191
201
|
|
|
@@ -60,7 +60,7 @@ function okResult(content: string): CallLLMResult {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function failResult(error: string, stopReason?: "error" | "aborted"): CallLLMResult {
|
|
63
|
-
return { ok: false, error,
|
|
63
|
+
return { ok: false, error, ...(stopReason !== undefined ? { stopReason } : {}) };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** 构造「永不 resolve」的 mock callLLM(用于 timeout/abort 测试) */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 关键设计:
|
|
9
9
|
* - 依赖注入 ClassifierDeps(callLLM 注入,便于测试 mock,不直接 import)
|
|
10
10
|
* - C1a 收口:LLM 调用走 llm-shared callLLM(内部完成凭证 getApiKeyAndHeaders +
|
|
11
|
-
* completeSimple + stopReason 归一化:error/aborted → {ok:false,
|
|
11
|
+
* completeSimple + stopReason 归一化:error/aborted → {ok:false,
|
|
12
12
|
* stopReason 透传}),不再走 production.ts 的 streamSimple
|
|
13
13
|
* - G3 修正(由 callLLM 承接):completeSimple 的 EventStream.result() 只 resolve 不 reject,
|
|
14
14
|
* error/aborted 也 resolve(带 stopReason)。callLLM 已把 error/aborted 归一为 ok:false +
|
|
@@ -90,7 +90,7 @@ function raceResultWithDeadline(
|
|
|
90
90
|
(result) => ({ kind: "ok" as const, result }),
|
|
91
91
|
(error) => ({
|
|
92
92
|
kind: "ok" as const,
|
|
93
|
-
result: { ok: false as const, error: error instanceof Error ? error.message : String(error)
|
|
93
|
+
result: { ok: false as const, error: error instanceof Error ? error.message : String(error) },
|
|
94
94
|
}),
|
|
95
95
|
),
|
|
96
96
|
];
|
|
@@ -119,6 +119,78 @@ function raceResultWithDeadline(
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// ──────────────────────── classifyRisk 分步 helper ────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 构造 CallLLMOptions(timeout 秒→毫秒的换算由调用方完成后传入;
|
|
126
|
+
* signal 透传 callLLM → completeSimple,abort 时 reject 或 resolve(aborted)
|
|
127
|
+
* 都会归一为 ok:false)。
|
|
128
|
+
*/
|
|
129
|
+
function buildCallOptions(
|
|
130
|
+
model: Model<Api>,
|
|
131
|
+
ctx: ToolInvocationContext,
|
|
132
|
+
config: ClassifierConfig,
|
|
133
|
+
timeoutMs: number | undefined,
|
|
134
|
+
signal: AbortSignal | undefined,
|
|
135
|
+
): CallLLMOptions {
|
|
136
|
+
return {
|
|
137
|
+
model,
|
|
138
|
+
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
139
|
+
messages: buildMessages(ctx),
|
|
140
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
141
|
+
...(signal !== undefined ? { signal } : {}),
|
|
142
|
+
// thinkingLevel 直接透传(含 "off");llm-shared 内部会把 "off" 映射为不传 reasoning(provider 默认)
|
|
143
|
+
reasoning: config.thinkingLevel,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 记录 LLM 调用失败日志(G3 语义:abort 与 error 分开记,即使行为上两者都 fallback 无差别)。 */
|
|
148
|
+
function logFailedCall(
|
|
149
|
+
result: Extract<CallLLMResult, { ok: false }>,
|
|
150
|
+
onLog?: (msg: string) => void,
|
|
151
|
+
): void {
|
|
152
|
+
if (result.stopReason === "aborted") {
|
|
153
|
+
onLog?.(`[pi-permission] classifier: LLM call aborted (stopReason=aborted), returning fallback`);
|
|
154
|
+
} else {
|
|
155
|
+
const stopDetail = result.stopReason !== undefined ? ` (stopReason=${result.stopReason})` : "";
|
|
156
|
+
onLog?.(`[pi-permission] classifier: LLM call failed: ${result.error}${stopDetail}, returning fallback`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** raceWithDeadline 已收窄的结果形态(timeout/aborted 无 result 字段)。 */
|
|
161
|
+
type SettledRace =
|
|
162
|
+
| { kind: "ok"; result: CallLLMResult }
|
|
163
|
+
| { kind: "timeout" }
|
|
164
|
+
| { kind: "aborted" };
|
|
165
|
+
|
|
166
|
+
/** settleRacedResult 的输出:fallback = fail-closed ask;ok = 继续解析文本。 */
|
|
167
|
+
type SettledOutcome =
|
|
168
|
+
| { kind: "fallback"; result: ClassifierResult }
|
|
169
|
+
| { kind: "ok"; result: Extract<CallLLMResult, { ok: true }> };
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* race 结果归一:timeout / aborted / ok:false → fail-closed fallback(保留日志区分),
|
|
173
|
+
* 成功原样透传(调用方收窄后取 content)。
|
|
174
|
+
*/
|
|
175
|
+
function settleRacedResult(settled: SettledRace, timeoutMs: number | undefined, onLog?: (msg: string) => void): SettledOutcome {
|
|
176
|
+
if (settled.kind === "timeout") {
|
|
177
|
+
onLog?.(`[pi-permission] classifier: timed out after ${timeoutMs}ms`);
|
|
178
|
+
return { kind: "fallback", result: { ...CLASSIFY_FALLBACK_RESULT } };
|
|
179
|
+
}
|
|
180
|
+
if (settled.kind === "aborted") {
|
|
181
|
+
onLog?.(`[pi-permission] classifier: aborted by signal`);
|
|
182
|
+
return { kind: "fallback", result: { ...CLASSIFY_FALLBACK_RESULT } };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ok:false → fallback(fail-closed)。stopReason 独立透传字段保留日志区分
|
|
186
|
+
const result = settled.result;
|
|
187
|
+
if (!result.ok) {
|
|
188
|
+
logFailedCall(result, onLog);
|
|
189
|
+
return { kind: "fallback", result: { ...CLASSIFY_FALLBACK_RESULT } };
|
|
190
|
+
}
|
|
191
|
+
return { kind: "ok", result };
|
|
192
|
+
}
|
|
193
|
+
|
|
122
194
|
// ──────────────────────── createClassifier ────────────────────────
|
|
123
195
|
|
|
124
196
|
/**
|
|
@@ -148,21 +220,12 @@ export function createClassifier(deps: ClassifierDeps): {
|
|
|
148
220
|
// 实证 classifier 真实用到 OAuth/配置的模型(而非 fail-closed 降级)。
|
|
149
221
|
onLog?.(`[pi-permission] classifier: using model ${model.id}`);
|
|
150
222
|
|
|
151
|
-
// 2. 构造 CallLLMOptions
|
|
152
|
-
// abort 时 reject 或 resolve(aborted) 都会归一为 ok:false)
|
|
223
|
+
// 2. 构造 CallLLMOptions
|
|
153
224
|
const timeoutMs = config.timeout > 0 ? config.timeout * MILLIS_PER_SECOND : undefined;
|
|
154
|
-
const callPromise = callLLM(
|
|
155
|
-
model,
|
|
156
|
-
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
157
|
-
messages: buildMessages(ctx),
|
|
158
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
159
|
-
...(signal !== undefined ? { signal } : {}),
|
|
160
|
-
// thinkingLevel 直接透传(含 "off");llm-shared 内部会把 "off" 映射为不传 reasoning(provider 默认)
|
|
161
|
-
reasoning: config.thinkingLevel,
|
|
162
|
-
});
|
|
225
|
+
const callPromise = callLLM(buildCallOptions(model, ctx, config, timeoutMs, signal));
|
|
163
226
|
|
|
164
227
|
// 3. 等待 callLLM + 外层超时/中止兜底(provider 不支持 timeoutMs/signal 时防永挂)
|
|
165
|
-
let settled:
|
|
228
|
+
let settled: SettledRace;
|
|
166
229
|
try {
|
|
167
230
|
settled = await raceResultWithDeadline(callPromise, timeoutMs, signal);
|
|
168
231
|
} catch (error) {
|
|
@@ -171,30 +234,14 @@ export function createClassifier(deps: ClassifierDeps): {
|
|
|
171
234
|
return { ...CLASSIFY_FALLBACK_RESULT };
|
|
172
235
|
}
|
|
173
236
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (settled.kind === "aborted") {
|
|
179
|
-
onLog?.(`[pi-permission] classifier: aborted by signal`);
|
|
180
|
-
return { ...CLASSIFY_FALLBACK_RESULT };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 4. ok:false → fallback(fail-closed)。stopReason 独立透传字段保留日志区分
|
|
184
|
-
// (G3 语义:abort 与 error 分开记,即使行为上两者都 fallback 无差别)。
|
|
185
|
-
const result = settled.result;
|
|
186
|
-
if (!result.ok) {
|
|
187
|
-
if (result.stopReason === "aborted") {
|
|
188
|
-
onLog?.(`[pi-permission] classifier: LLM call aborted (stopReason=aborted), returning fallback`);
|
|
189
|
-
} else {
|
|
190
|
-
const stopDetail = result.stopReason !== undefined ? ` (stopReason=${result.stopReason})` : "";
|
|
191
|
-
onLog?.(`[pi-permission] classifier: LLM call failed: ${result.error}${stopDetail}, returning fallback`);
|
|
192
|
-
}
|
|
193
|
-
return { ...CLASSIFY_FALLBACK_RESULT };
|
|
237
|
+
// 4. timeout / aborted / ok:false → fail-closed fallback;成功 → 收窄透传
|
|
238
|
+
const settledOutcome = settleRacedResult(settled, timeoutMs, onLog);
|
|
239
|
+
if (settledOutcome.kind === "fallback") {
|
|
240
|
+
return settledOutcome.result;
|
|
194
241
|
}
|
|
195
242
|
|
|
196
243
|
// 5. 提取文本 → 三层容错解析
|
|
197
|
-
return parseClassifierResponse(result.content);
|
|
244
|
+
return parseClassifierResponse(settledOutcome.result.content);
|
|
198
245
|
}
|
|
199
246
|
|
|
200
247
|
return { classifyRisk };
|
|
@@ -64,14 +64,11 @@ export interface ResolvedModelEntry {
|
|
|
64
64
|
* 「无可选模型」提示)。
|
|
65
65
|
*
|
|
66
66
|
* @param ctx 含 modelRegistry 的上下文(model 列表 + auth 判定)
|
|
67
|
-
* @param onWarning 保留参数位(契约兼容;E2 后无文件读取,无警告源)
|
|
68
67
|
* @returns Map<providerName, ResolvedModelEntry[]>
|
|
69
68
|
*/
|
|
70
69
|
export function listAvailableModels(
|
|
71
70
|
ctx: ListAvailableModelsCtx,
|
|
72
|
-
onWarning?: (msg: string) => void,
|
|
73
71
|
): Map<string, ResolvedModelEntry[]> {
|
|
74
|
-
void onWarning; // 无警告源(不再读文件),保留参数位兼容调用方
|
|
75
72
|
const entries: ResolvedModelEntry[] = [];
|
|
76
73
|
for (const m of ctx.modelRegistry.getAll()) {
|
|
77
74
|
if (!ctx.modelRegistry.hasConfiguredAuth(m)) continue;
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ExtensionContext,
|
|
19
19
|
getAgentDir,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { oncePerProcess } from "@zhushanwen/pi-ext-guards";
|
|
21
22
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
22
23
|
import { migrateLegacyConfig } from "@zhushanwen/pi-llm-shared";
|
|
23
24
|
|
|
@@ -44,8 +45,9 @@ import { paletteFromTheme, type PermissionPalette } from "./statusline-palette.j
|
|
|
44
45
|
// Added in v1.0.0. Remove after v2.0.0 (one major past).
|
|
45
46
|
// session_start 首次触发时把旧路径 permission-config.json 迁到 config/permission-ext-config.json。
|
|
46
47
|
// 幂等、best-effort(migrateLegacyConfig 见 @zhushanwen/pi-llm-shared)。
|
|
47
|
-
//
|
|
48
|
-
|
|
48
|
+
// 写 agentDir 全局配置文件属跨 session 副作用——oncePerProcess 守卫防 factory 二调/handler
|
|
49
|
+
// 累积双跑(u-audit-fix 收编原内联 once flag;key 带包名前缀避撞,见 pi-ext-guards JSDoc)。
|
|
50
|
+
// agentDir 由 getAgentDir() 推导(尊重 PI_CODING_AGENT_DIR)。
|
|
49
51
|
|
|
50
52
|
/** 默认配置 warning 回调(loadAndWatchConfig 共用,透传 logger.warn)。 */
|
|
51
53
|
const defaultConfigWarn = (msg: string): void => logger.warn(msg);
|
|
@@ -86,10 +88,8 @@ const _UI_OPTIONS_PARAM_INDEX = 2;
|
|
|
86
88
|
*/
|
|
87
89
|
export default function permissionExtension(pi: ExtensionAPI): void {
|
|
88
90
|
// W7:注入 listAvailableModels 真实实现(model-picker.ts 默认返回空 Map)。
|
|
89
|
-
// E2 签名:(ctx
|
|
90
|
-
setDefaultListAvailableModels((ctx
|
|
91
|
-
listAvailableModels(ctx, onWarning ?? defaultConfigWarn),
|
|
92
|
-
);
|
|
91
|
+
// E2 签名:(ctx) → ctx.modelRegistry.getAll() + hasConfiguredAuth 过滤。
|
|
92
|
+
setDefaultListAvailableModels((ctx) => listAvailableModels(ctx));
|
|
93
93
|
|
|
94
94
|
// ──────────────────────── 配置读取(读时刷新,回归 llm-shared 框架) ────────────────────────
|
|
95
95
|
// 不持有跨调用缓存:每次需要配置直接 loadAndWatchConfig(),llm-shared 内部 mtime+size 去重,
|
|
@@ -100,10 +100,9 @@ export default function permissionExtension(pi: ExtensionAPI): void {
|
|
|
100
100
|
// ──────────────────────── session_start:迁移旧路径配置 ────────────────────────
|
|
101
101
|
pi.on("session_start", (_event: unknown, ctx: ExtensionContext) => {
|
|
102
102
|
// [MIGRATION] Added in v1.0.0. Remove after v2.0.0.
|
|
103
|
-
|
|
104
|
-
configMigrationChecked = true;
|
|
103
|
+
oncePerProcess("permission:migrate-legacy-config", () => {
|
|
105
104
|
migrateLegacyConfig(getAgentDir(), "permission-config.json", "config/permission-ext-config.json");
|
|
106
|
-
}
|
|
105
|
+
});
|
|
107
106
|
// 无需手动刷新:去闭包后每次 loadAndWatchConfig 读时刷新;迁移改写文件后下次读取自动生效。
|
|
108
107
|
// 注册 footer line renderer(consumer 端握手,pi-statusline 是 canonical owner)。
|
|
109
108
|
// renderer 无状态:render 时用 statusline 传入的 theme + loadAndWatchConfig 读最新 config。
|
|
@@ -195,7 +194,7 @@ export default function permissionExtension(pi: ExtensionAPI): void {
|
|
|
195
194
|
},
|
|
196
195
|
config,
|
|
197
196
|
{
|
|
198
|
-
listModels: (pickerCtx) => listAvailableModels(pickerCtx
|
|
197
|
+
listModels: (pickerCtx) => listAvailableModels(pickerCtx),
|
|
199
198
|
save: (newConfig) => {
|
|
200
199
|
const r = saveConfig(newConfig);
|
|
201
200
|
if (r.success) requestFooterRender();
|
package/src/model-picker.ts
CHANGED
|
@@ -23,6 +23,10 @@ import { type Component, Container, type SelectItem, SelectList, type SelectList
|
|
|
23
23
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
24
24
|
|
|
25
25
|
import type { ResolvedModelEntry } from "./classifier/model-resolver.js";
|
|
26
|
+
import { DEFAULT_SELECT_THEME } from "./select-theme.js";
|
|
27
|
+
|
|
28
|
+
// 保持既有 public 名(model-picker.test.ts MPT7 直接消费此路径)。
|
|
29
|
+
export { DEFAULT_SELECT_THEME };
|
|
26
30
|
|
|
27
31
|
// ──────────────────────── 类型 ────────────────────────
|
|
28
32
|
|
|
@@ -31,22 +35,6 @@ export type SelectionResult =
|
|
|
31
35
|
| { kind: "auto" }
|
|
32
36
|
| { kind: "specific"; provider: string; modelId: string };
|
|
33
37
|
|
|
34
|
-
// ──────────────────────── DEFAULT_SELECT_THEME(G2 修正) ────────────────────────
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* SelectList 默认主题。
|
|
38
|
-
*
|
|
39
|
-
* G2/WR2 修正:selectedPrefix 实现为 `(t) => '▶ ' + t`(非 identity),
|
|
40
|
-
* 选中行有视觉区分(▶ 前缀)。
|
|
41
|
-
*/
|
|
42
|
-
export const DEFAULT_SELECT_THEME: SelectListTheme = {
|
|
43
|
-
selectedPrefix: (t: string): string => "\u25B6 " + t,
|
|
44
|
-
selectedText: (t: string): string => t,
|
|
45
|
-
description: (t: string): string => t,
|
|
46
|
-
scrollInfo: (t: string): string => t,
|
|
47
|
-
noMatch: (t: string): string => t,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
38
|
/** provider stage 的 'auto' 项 value(用特殊标记区分真实 provider 名)。 */
|
|
51
39
|
const AUTO_VALUE = "__auto__";
|
|
52
40
|
|
|
@@ -189,7 +177,7 @@ export class ProviderModelSelectorComponent extends Container {
|
|
|
189
177
|
/** provider stage items:'Auto' + 各 provider(含 model 数量)。 */
|
|
190
178
|
private buildProviderItems(): SelectItem[] {
|
|
191
179
|
const items: SelectItem[] = [
|
|
192
|
-
{ value: AUTO_VALUE, label: "Auto", description: "
|
|
180
|
+
{ value: AUTO_VALUE, label: "Auto", description: "select first available model" },
|
|
193
181
|
];
|
|
194
182
|
for (const provider of this.providers) {
|
|
195
183
|
const models = this.modelsByProvider.get(provider) ?? [];
|
|
@@ -297,7 +285,6 @@ export interface ModelPickerContext {
|
|
|
297
285
|
/** listAvailableModels 注入签名(E2 改走 ctx.modelRegistry,签名带 ctx;便于测试 mock)。 */
|
|
298
286
|
export type ListAvailableModelsFn = (
|
|
299
287
|
ctx: ModelPickerContext,
|
|
300
|
-
onWarning?: (msg: string) => void,
|
|
301
288
|
) => Map<string, ResolvedModelEntry[]>;
|
|
302
289
|
|
|
303
290
|
/**
|