@zhushanwen/pi-permission 0.0.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 +455 -0
- package/index.ts +2 -0
- package/package.json +52 -0
- package/src/__tests__/approval.test.ts +456 -0
- package/src/__tests__/commands.test.ts +487 -0
- package/src/__tests__/config.test.ts +229 -0
- package/src/__tests__/e2e-modes.test.ts +253 -0
- package/src/__tests__/extension.test.ts +170 -0
- package/src/__tests__/index-integration.test.ts +282 -0
- package/src/__tests__/model-picker.test.ts +371 -0
- package/src/__tests__/pipeline.test.ts +976 -0
- package/src/__tests__/production.test.ts +102 -0
- package/src/__tests__/rule-editor-component.test.ts +475 -0
- package/src/__tests__/rule-editor.test.ts +252 -0
- package/src/__tests__/rule-templates.test.ts +264 -0
- package/src/__tests__/statusline.test.ts +221 -0
- package/src/__tests__/types.test.ts +59 -0
- package/src/approval.ts +352 -0
- package/src/ast/__tests__/analyzer.test.ts +262 -0
- package/src/ast/__tests__/loader.test.ts +136 -0
- package/src/ast/analyzer.ts +326 -0
- package/src/ast/index.ts +7 -0
- package/src/ast/loader.ts +113 -0
- package/src/classifier/__tests__/classifier.test.ts +285 -0
- package/src/classifier/__tests__/json-parser.test.ts +118 -0
- package/src/classifier/__tests__/model-resolver.test.ts +173 -0
- package/src/classifier/__tests__/prompt.test.ts +59 -0
- package/src/classifier/classifier.ts +242 -0
- package/src/classifier/index.ts +28 -0
- package/src/classifier/json-parser.ts +127 -0
- package/src/classifier/model-resolver.ts +295 -0
- package/src/classifier/prompt.ts +52 -0
- package/src/commands.ts +234 -0
- package/src/config.ts +216 -0
- package/src/index.ts +291 -0
- package/src/model-picker.ts +398 -0
- package/src/pipeline.ts +541 -0
- package/src/production.ts +102 -0
- package/src/rule-editor-component.ts +883 -0
- package/src/rule-editor.ts +262 -0
- package/src/rule-templates.ts +332 -0
- package/src/rules/__tests__/builtins.test.ts +614 -0
- package/src/rules/__tests__/index.test.ts +26 -0
- package/src/rules/__tests__/matcher.test.ts +378 -0
- package/src/rules/__tests__/wildcard.test.ts +84 -0
- package/src/rules/builtins.ts +680 -0
- package/src/rules/index.ts +27 -0
- package/src/rules/matcher.ts +160 -0
- package/src/rules/wildcard.ts +43 -0
- package/src/statusline.ts +226 -0
- package/src/types.ts +249 -0
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W8: RuleEditorComponent — TUI overlay for CRUD rule editing.
|
|
3
|
+
*
|
|
4
|
+
* 3-stage state machine(list/add/fill),Container override handleInput 委托。
|
|
5
|
+
* G1:list → add → fill(无 delete-confirm,fill edit 模式底部 [Delete this rule])。
|
|
6
|
+
* G2:currentListRules = applyOps(initialRules, ops),实时反映 ops。
|
|
7
|
+
* G4:pattern 用 wildcard(`npm *`),不用正则。
|
|
8
|
+
* G8:build 不生成 id。
|
|
9
|
+
* G9:tool 只列 bash。
|
|
10
|
+
* G13:sessionIdCounter 立即赋值真实 id。
|
|
11
|
+
* WR8:focusIndex 模 5(Custom form 5 项 Tab 循环)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type Component, Container, Input, type SelectItem, SelectList, type SelectListTheme, truncateToWidth } from "@earendil-works/pi-tui";
|
|
15
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
ALL_TEMPLATES,
|
|
19
|
+
applyOps,
|
|
20
|
+
PRESET_COMMANDS,
|
|
21
|
+
type RuleOp,
|
|
22
|
+
type RuleTemplate,
|
|
23
|
+
type TemplateSelections,
|
|
24
|
+
} from "./rule-templates.js";
|
|
25
|
+
import type { PermissionAction, Rule } from "./types.js";
|
|
26
|
+
|
|
27
|
+
// ──────────────────────── DEFAULT_SELECT_THEME ────────────────────────
|
|
28
|
+
|
|
29
|
+
const THEME: SelectListTheme = {
|
|
30
|
+
selectedPrefix: (t: string): string => "\u25B6 " + t,
|
|
31
|
+
selectedText: (t: string): string => t,
|
|
32
|
+
description: (t: string): string => t,
|
|
33
|
+
scrollInfo: (t: string): string => t,
|
|
34
|
+
noMatch: (t: string): string => t,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const MAX_VISIBLE = 12;
|
|
38
|
+
|
|
39
|
+
// ──────────────────────── TextLines component ────────────────────────
|
|
40
|
+
|
|
41
|
+
class TextLines implements Component {
|
|
42
|
+
private readonly lines: string[];
|
|
43
|
+
|
|
44
|
+
constructor(lines: string[]) {
|
|
45
|
+
this.lines = lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
render(_width: number): string[] {
|
|
49
|
+
return [...this.lines];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
invalidate(): void {
|
|
53
|
+
// no-op
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ──────────────────────── Stage types ────────────────────────
|
|
58
|
+
|
|
59
|
+
type Stage = "list" | "add" | "fill";
|
|
60
|
+
|
|
61
|
+
/** fill stage 子类型(按模板分支)。 */
|
|
62
|
+
type FillKind =
|
|
63
|
+
| "command-select" // allow-family / ask-before:命令选择 → commitFill
|
|
64
|
+
| "deny-family" // deny-family:命令 → 子命令 → commitFill
|
|
65
|
+
| "allow-subcmd" // allow-subcmd:命令 → Input 子命令 → commitFill
|
|
66
|
+
| "custom"; // custom:5 项 form + Tab 焦点
|
|
67
|
+
|
|
68
|
+
// ──────────────────────── RuleEditorComponent ────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* TUI overlay 组件:规则 CRUD 编辑器。
|
|
72
|
+
*
|
|
73
|
+
* Container override handleInput(W7 模式):按 stage 委托给当前子组件。
|
|
74
|
+
* settle 用 _resolved 守卫防二次 done。
|
|
75
|
+
*/
|
|
76
|
+
export class RuleEditorComponent extends Container {
|
|
77
|
+
private stage: Stage = "list";
|
|
78
|
+
private readonly initialRules: readonly Rule[];
|
|
79
|
+
private readonly sessionIdCounter: () => string;
|
|
80
|
+
private readonly done: (ops: RuleOp[]) => void;
|
|
81
|
+
private readonly theme: SelectListTheme;
|
|
82
|
+
private _resolved = false;
|
|
83
|
+
|
|
84
|
+
/** 累积的操作列表(add/edit/delete)。 */
|
|
85
|
+
ops: RuleOp[] = [];
|
|
86
|
+
|
|
87
|
+
/** 当前活跃子组件引用(用于 handleInput 委托)。 */
|
|
88
|
+
private currentList: SelectList | null = null;
|
|
89
|
+
private currentInput: Input | null = null;
|
|
90
|
+
|
|
91
|
+
/** fill stage 状态。 */
|
|
92
|
+
private fillKind: FillKind = "command-select";
|
|
93
|
+
private fillTemplate: RuleTemplate | null = null;
|
|
94
|
+
private fillEditMode = false;
|
|
95
|
+
private fillEditRuleId: string | null = null;
|
|
96
|
+
private fillSelections: TemplateSelections = {};
|
|
97
|
+
|
|
98
|
+
/** deny-family 子阶段:'cmd' = 选命令,'subcmd' = 选子命令。 */
|
|
99
|
+
private denySubStage: "cmd" | "subcmd" = "cmd";
|
|
100
|
+
|
|
101
|
+
/** Custom form:5 项焦点索引(WR8:focusIndex 模 5)。 */
|
|
102
|
+
private customFocusIndex = 0;
|
|
103
|
+
private customChildren: Component[] = [];
|
|
104
|
+
|
|
105
|
+
/** 搜索状态(command-select 界面)。 */
|
|
106
|
+
private _searchInput: Input | null = null;
|
|
107
|
+
private _commandList: SelectList | null = null;
|
|
108
|
+
private _searchFocus = false; // true = 搜索框获焦,false = 列表获焦
|
|
109
|
+
|
|
110
|
+
constructor(
|
|
111
|
+
initialRules: readonly Rule[],
|
|
112
|
+
sessionIdCounter: () => string,
|
|
113
|
+
done: (ops: RuleOp[]) => void,
|
|
114
|
+
theme: SelectListTheme = THEME,
|
|
115
|
+
) {
|
|
116
|
+
super();
|
|
117
|
+
this.initialRules = initialRules;
|
|
118
|
+
this.sessionIdCounter = sessionIdCounter;
|
|
119
|
+
this.done = done;
|
|
120
|
+
this.theme = theme;
|
|
121
|
+
this.switchToListStage();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ──────────────────────── 边框渲染 ────────────────────────
|
|
125
|
+
|
|
126
|
+
/** box 边框左右各占用 1 列(│ × 2) */
|
|
127
|
+
private static readonly BORDER_OVERHEAD = 2;
|
|
128
|
+
|
|
129
|
+
override render(width: number): string[] {
|
|
130
|
+
const innerWidth = Math.max(0, width - RuleEditorComponent.BORDER_OVERHEAD);
|
|
131
|
+
let inner = super.render(innerWidth);
|
|
132
|
+
|
|
133
|
+
// custom form 焦点指示(M4):当前焦点字段的标签加 ▶ 前缀
|
|
134
|
+
if (this.stage === "fill" && this.fillKind === "custom") {
|
|
135
|
+
inner = this._injectFocusIndicator(inner);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
lines.push(`\u250C${"\u2500".repeat(innerWidth)}\u2510`);
|
|
140
|
+
for (const line of inner) {
|
|
141
|
+
const padded = truncateToWidth(line, innerWidth, "", true);
|
|
142
|
+
lines.push(`\u2502${padded}\u2502`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(`\u2514${"\u2500".repeat(innerWidth)}\u2518`);
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 在 custom form 渲染输出中给当前焦点字段的标签行加 ▶ 前缀(M4)。
|
|
150
|
+
*
|
|
151
|
+
* TextLines.render 输出原始未 padding 的标签行,故可精确匹配。
|
|
152
|
+
* 字段标签(与 startFillCustom 里 addChild 的 TextLines 一致):
|
|
153
|
+
* customFocusIndex 0 → "Pattern (wildcard, e.g. 'npm *')"
|
|
154
|
+
* customFocusIndex 1 → "Action"
|
|
155
|
+
* customFocusIndex 2 → "Tool"
|
|
156
|
+
* customFocusIndex 3 → "Description (optional)"
|
|
157
|
+
* index 4 (Submit) / 5 (Delete) 为 SelectList,无文字标签,不处理。
|
|
158
|
+
*/
|
|
159
|
+
private _injectFocusIndicator(lines: string[]): string[] {
|
|
160
|
+
const fieldLabels = [
|
|
161
|
+
"Pattern (wildcard, e.g. 'npm *')", // index 0
|
|
162
|
+
"Action", // index 1
|
|
163
|
+
"Tool", // index 2
|
|
164
|
+
"Description (optional)", // index 3
|
|
165
|
+
];
|
|
166
|
+
const focusedLabel = fieldLabels[this.customFocusIndex];
|
|
167
|
+
if (focusedLabel === undefined) return lines; // Submit/Delete 焦点:不加标记
|
|
168
|
+
|
|
169
|
+
return lines.map((line) => {
|
|
170
|
+
// 精确匹配当前焦点标签 → 加 ▶ 前缀
|
|
171
|
+
if (line === focusedLabel) {
|
|
172
|
+
return `\u25B6 ${line}`;
|
|
173
|
+
}
|
|
174
|
+
// 焦点已转移:去掉其他标签行残留的 ▶ 前缀(避免重复渲染累积)
|
|
175
|
+
if (line.startsWith("\u25B6 ") && fieldLabels.includes(line.slice(2))) {
|
|
176
|
+
return line.slice(2);
|
|
177
|
+
}
|
|
178
|
+
return line;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ──────────────────────── handleInput override(Container 无 handleInput,必须新增) ────────────────────────
|
|
183
|
+
|
|
184
|
+
handleInput(data: string): void {
|
|
185
|
+
// Custom form:Tab 焦点路由(WR8)
|
|
186
|
+
if (this.stage === "fill" && this.fillKind === "custom") {
|
|
187
|
+
if (matchesKey(data, "tab")) {
|
|
188
|
+
// ES2 修复:以 customChildren.length 为模,避免硬编码与实际 children 数量不一致。
|
|
189
|
+
// New 模式 5 项(pattern/action/tool/desc/submit),Edit 模式 6 项(+delete)。
|
|
190
|
+
const maxIndex = this.customChildren.length;
|
|
191
|
+
if (maxIndex === 0) return; // 防御:无子组件时不循环
|
|
192
|
+
this.customFocusIndex = (this.customFocusIndex + 1) % maxIndex;
|
|
193
|
+
this.syncCustomFocus();
|
|
194
|
+
this.invalidate();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// 委托给当前 focused 子组件
|
|
198
|
+
const child = this.customChildren[this.customFocusIndex];
|
|
199
|
+
if (child !== undefined && child.handleInput !== undefined) {
|
|
200
|
+
child.handleInput(data);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// command-select 界面:Tab 切换搜索框 ↔ 列表
|
|
206
|
+
if (this.stage === "fill" && this._searchInput !== null && this._commandList !== null) {
|
|
207
|
+
if (matchesKey(data, "tab")) {
|
|
208
|
+
this._searchFocus = !this._searchFocus;
|
|
209
|
+
this._searchInput.focused = this._searchFocus;
|
|
210
|
+
this.invalidate();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// 根据焦点委托
|
|
214
|
+
if (this._searchFocus) {
|
|
215
|
+
// 搜索框获焦:输入字符时实时过滤列表
|
|
216
|
+
this._searchInput.handleInput(data);
|
|
217
|
+
// onSubmit(Enter)可能在 handleInput 内触发 stage 转换
|
|
218
|
+
// (_resetStageRefs 会清 _searchInput),此时本轮渲染已由转换处理,直接返回。
|
|
219
|
+
if (this._searchInput === null) return;
|
|
220
|
+
// 使用 SelectList.setFilter 过滤
|
|
221
|
+
const query = this._searchInput.getValue();
|
|
222
|
+
this._commandList.setFilter(query);
|
|
223
|
+
this.invalidate();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// 列表获焦
|
|
227
|
+
this._commandList.handleInput(data);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 独立 Input 组件(startCommandInput / startSubcmdInput)
|
|
232
|
+
if (this.stage === "fill" && this.currentInput !== null) {
|
|
233
|
+
this.currentInput.handleInput(data);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 非 custom form:委托给当前 active 组件
|
|
238
|
+
this.currentList?.handleInput(data);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 退出(外部 abort 用)。 */
|
|
242
|
+
cancel(): void {
|
|
243
|
+
this.settle();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** settle:_resolved 守卫 + done(this.ops)。 */
|
|
247
|
+
private settle(): void {
|
|
248
|
+
if (this._resolved) return;
|
|
249
|
+
this._resolved = true;
|
|
250
|
+
this.done(this.ops);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 集中清理子组件引用(C3 根因修复)。
|
|
255
|
+
*
|
|
256
|
+
* Container.clear() 只清 children 数组,不清组件引用字段。
|
|
257
|
+
* 每个 switchTo* 或 startFill* 方法在 this.clear() 之后必须调用本方法,
|
|
258
|
+
* 杜绝手动清理遗漏导致的输入路由死锁(C1)。
|
|
259
|
+
*
|
|
260
|
+
* 注意:不清业务状态字段(fillKind/fillEditMode/fillEditRuleId/fillSelections/
|
|
261
|
+
* fillTemplate/denySubStage/customFocusIndex)——这些由各自的 enter*Mode 方法显式设置。
|
|
262
|
+
*/
|
|
263
|
+
private _resetStageRefs(): void {
|
|
264
|
+
this._searchInput = null;
|
|
265
|
+
this._commandList = null;
|
|
266
|
+
this._searchFocus = false;
|
|
267
|
+
this.currentInput = null;
|
|
268
|
+
this.currentList = null;
|
|
269
|
+
this.customChildren = [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** currentListRules = applyOps(initialRules, ops)(G2:实时反映 ops)。 */
|
|
273
|
+
currentListRules(): Rule[] {
|
|
274
|
+
return applyOps([...this.initialRules], this.ops);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ──────────────────────── list stage ────────────────────────
|
|
278
|
+
|
|
279
|
+
private switchToListStage(): void {
|
|
280
|
+
this.stage = "list";
|
|
281
|
+
this.clear();
|
|
282
|
+
this._resetStageRefs();
|
|
283
|
+
this.currentList = this.buildListStage();
|
|
284
|
+
this.addChild(new TextLines(["[pi-permission] Permission Rules", ""]));
|
|
285
|
+
this.addChild(this.currentList);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private buildListStage(): SelectList {
|
|
289
|
+
const rules = this.currentListRules();
|
|
290
|
+
const items: SelectItem[] = [];
|
|
291
|
+
|
|
292
|
+
for (const rule of rules) {
|
|
293
|
+
const desc = rule.description ?? rule.pattern;
|
|
294
|
+
items.push({
|
|
295
|
+
value: `rule:${rule.id}`,
|
|
296
|
+
label: `[${rule.action}] ${rule.pattern}`,
|
|
297
|
+
description: desc,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
items.push({ value: "__add__", label: "[+ Add rule]", description: "Add a new permission rule" });
|
|
302
|
+
items.push({ value: "__done__", label: "[Done]", description: "Save and exit" });
|
|
303
|
+
|
|
304
|
+
const list = new SelectList(items, MAX_VISIBLE, this.theme);
|
|
305
|
+
list.onSelect = (item: SelectItem): void => {
|
|
306
|
+
if (item.value === "__add__") {
|
|
307
|
+
this.switchToAddStage();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (item.value === "__done__") {
|
|
311
|
+
this.settle();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// 进入 edit 模式
|
|
315
|
+
const ruleId = item.value.replace(/^rule:/, "");
|
|
316
|
+
this.enterFillEditMode(ruleId);
|
|
317
|
+
};
|
|
318
|
+
list.onCancel = (): void => {
|
|
319
|
+
this.settle();
|
|
320
|
+
};
|
|
321
|
+
return list;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ──────────────────────── add stage ────────────────────────
|
|
325
|
+
|
|
326
|
+
private switchToAddStage(): void {
|
|
327
|
+
this.stage = "add";
|
|
328
|
+
this.clear();
|
|
329
|
+
this._resetStageRefs();
|
|
330
|
+
const items: SelectItem[] = ALL_TEMPLATES.map((t) => ({
|
|
331
|
+
value: t.id,
|
|
332
|
+
label: t.label,
|
|
333
|
+
description: t.description,
|
|
334
|
+
}));
|
|
335
|
+
const list = new SelectList(items, MAX_VISIBLE, this.theme);
|
|
336
|
+
list.onSelect = (item: SelectItem): void => {
|
|
337
|
+
const template = ALL_TEMPLATES.find((t) => t.id === item.value);
|
|
338
|
+
if (template !== undefined) {
|
|
339
|
+
this.enterFillNewMode(template);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
list.onCancel = (): void => {
|
|
343
|
+
this.switchToListStage();
|
|
344
|
+
};
|
|
345
|
+
this.currentList = list;
|
|
346
|
+
this.addChild(new TextLines(["[pi-permission] Add Rule — Select Template", ""]));
|
|
347
|
+
this.addChild(list);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ──────────────────────── fill stage ────────────────────────
|
|
351
|
+
|
|
352
|
+
private enterFillNewMode(template: RuleTemplate): void {
|
|
353
|
+
this._resetStageRefs();
|
|
354
|
+
this.fillTemplate = template;
|
|
355
|
+
this.fillEditMode = false;
|
|
356
|
+
this.fillEditRuleId = null;
|
|
357
|
+
this.fillSelections = {};
|
|
358
|
+
this.startFillForTemplate(template);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private enterFillEditMode(ruleId: string): void {
|
|
362
|
+
this._resetStageRefs();
|
|
363
|
+
const rules = this.currentListRules();
|
|
364
|
+
const rule = rules.find((r) => r.id === ruleId);
|
|
365
|
+
if (rule === undefined) {
|
|
366
|
+
this.switchToListStage();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.fillEditMode = true;
|
|
370
|
+
this.fillEditRuleId = ruleId;
|
|
371
|
+
this.fillSelections = {
|
|
372
|
+
pattern: rule.pattern,
|
|
373
|
+
action: rule.action,
|
|
374
|
+
tool: rule.tool,
|
|
375
|
+
description: rule.description,
|
|
376
|
+
};
|
|
377
|
+
// edit 模式走 custom form(用户可编辑所有字段)
|
|
378
|
+
this.startFillCustom();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private startFillForTemplate(template: RuleTemplate): void {
|
|
382
|
+
switch (template.id) {
|
|
383
|
+
case "allow-family":
|
|
384
|
+
case "ask-before":
|
|
385
|
+
this.startFillCommandSelect("command-select");
|
|
386
|
+
break;
|
|
387
|
+
case "deny-family":
|
|
388
|
+
this.denySubStage = "cmd";
|
|
389
|
+
this.startFillDenyFamily();
|
|
390
|
+
break;
|
|
391
|
+
case "allow-subcmd":
|
|
392
|
+
this.startFillAllowSubcmd();
|
|
393
|
+
break;
|
|
394
|
+
case "custom":
|
|
395
|
+
this.startFillCustom();
|
|
396
|
+
break;
|
|
397
|
+
default:
|
|
398
|
+
this.switchToListStage();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── command-select(allow-family / ask-before)带搜索 ──
|
|
403
|
+
|
|
404
|
+
private startFillCommandSelect(kind: FillKind): void {
|
|
405
|
+
this.stage = "fill";
|
|
406
|
+
this.fillKind = kind;
|
|
407
|
+
this.clear();
|
|
408
|
+
this._resetStageRefs();
|
|
409
|
+
|
|
410
|
+
// 搜索输入框
|
|
411
|
+
const searchInput = new Input();
|
|
412
|
+
searchInput.setValue("");
|
|
413
|
+
this.currentInput = searchInput;
|
|
414
|
+
this._searchInput = searchInput;
|
|
415
|
+
this._searchFocus = true; // 默认焦点在搜索框
|
|
416
|
+
|
|
417
|
+
// 命令列表(初始显示全部)
|
|
418
|
+
const allItems = this.buildPresetCommandItemsWithOther();
|
|
419
|
+
const list = new SelectList(allItems, MAX_VISIBLE, this.theme);
|
|
420
|
+
this.currentList = list;
|
|
421
|
+
this._commandList = list;
|
|
422
|
+
|
|
423
|
+
// 搜索输入变化时过滤列表
|
|
424
|
+
searchInput.onSubmit = (val: string): void => {
|
|
425
|
+
// Enter 在搜索框中 = 选择第一个匹配项或进入手动输入
|
|
426
|
+
const filtered = this._filterCommands(allItems, val.trim());
|
|
427
|
+
if (filtered.length === 0 || (filtered.length === 1 && filtered[0]?.value === "__other__")) {
|
|
428
|
+
// 无匹配 → 进入手动输入
|
|
429
|
+
this.startCommandInput();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// 有匹配 → 选中第一个(非 Other)
|
|
433
|
+
const firstMatch = filtered.find((i) => i.value !== "__other__");
|
|
434
|
+
if (firstMatch !== undefined) {
|
|
435
|
+
this.onCommandSelected(firstMatch.value);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
searchInput.onEscape = (): void => {
|
|
439
|
+
this.switchToListStage();
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// 列表选择
|
|
443
|
+
list.onSelect = (item: SelectItem): void => {
|
|
444
|
+
if (item.value === "__other__") {
|
|
445
|
+
this.startCommandInput();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.onCommandSelected(item.value);
|
|
449
|
+
};
|
|
450
|
+
list.onCancel = (): void => {
|
|
451
|
+
this.switchToListStage();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// 渲染
|
|
455
|
+
this.addChild(new TextLines(["[pi-permission] Select command", ""]));
|
|
456
|
+
this.addChild(new TextLines(["Type to search · Tab to switch focus · Enter to select"]));
|
|
457
|
+
this.addChild(searchInput);
|
|
458
|
+
this.addChild(list);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 命令选中后:弹出子命令/全选 选择 */
|
|
462
|
+
private onCommandSelected(cmd: string): void {
|
|
463
|
+
// 弹出选择:整个命令家族 / 特定子命令
|
|
464
|
+
this.clear();
|
|
465
|
+
this._resetStageRefs();
|
|
466
|
+
|
|
467
|
+
const scopeItems: SelectItem[] = [
|
|
468
|
+
{ value: "__all__", label: `${cmd} * (all subcommands)`, description: `Allow/deny all invocations of ${cmd}` },
|
|
469
|
+
{ value: "__subcmd__", label: `${cmd} <subcommand> *`, description: `Allow/deny a specific subcommand` },
|
|
470
|
+
];
|
|
471
|
+
const list = new SelectList(scopeItems, 2, this.theme);
|
|
472
|
+
list.onSelect = (item: SelectItem): void => {
|
|
473
|
+
if (item.value === "__all__") {
|
|
474
|
+
this.fillSelections.cmd = cmd;
|
|
475
|
+
this.commitFill();
|
|
476
|
+
} else {
|
|
477
|
+
// 进入子命令输入
|
|
478
|
+
this.fillSelections.cmd = cmd;
|
|
479
|
+
this.startSubcmdInput();
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
list.onCancel = (): void => {
|
|
483
|
+
// 返回命令选择
|
|
484
|
+
this.startFillCommandSelect(this.fillKind);
|
|
485
|
+
};
|
|
486
|
+
this.currentList = list;
|
|
487
|
+
this.addChild(new TextLines(["[pi-permission] Select scope for " + cmd, ""]));
|
|
488
|
+
this.addChild(list);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** 构建命令列表 + Other(在顶部) */
|
|
492
|
+
private buildPresetCommandItemsWithOther(): SelectItem[] {
|
|
493
|
+
const items: SelectItem[] = [
|
|
494
|
+
{ value: "__other__", label: "[Type command manually]", description: "Tab here to type a custom command" },
|
|
495
|
+
];
|
|
496
|
+
for (const cmd of PRESET_COMMANDS) {
|
|
497
|
+
items.push({
|
|
498
|
+
value: cmd.cmd,
|
|
499
|
+
label: `${cmd.cmd} — ${cmd.category}`,
|
|
500
|
+
description: cmd.label,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return items;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** 过滤命令列表(Enter 时检查匹配用)。startsWith 与 SelectList.setFilter 一致(M5)。 */
|
|
507
|
+
private _filterCommands(items: SelectItem[], query: string): SelectItem[] {
|
|
508
|
+
if (query.length === 0) return items;
|
|
509
|
+
const lower = query.toLowerCase();
|
|
510
|
+
return items.filter((item) => {
|
|
511
|
+
if (item.value === "__other__") return true; // 始终保留 Other
|
|
512
|
+
return item.value.toLowerCase().startsWith(lower) || item.label.toLowerCase().startsWith(lower);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── deny-family(命令 → 子命令)──
|
|
517
|
+
|
|
518
|
+
private startFillDenyFamily(): void {
|
|
519
|
+
this.stage = "fill";
|
|
520
|
+
this.fillKind = "deny-family";
|
|
521
|
+
this.clear();
|
|
522
|
+
this._resetStageRefs();
|
|
523
|
+
|
|
524
|
+
if (this.denySubStage === "cmd") {
|
|
525
|
+
// 带搜索的命令选择
|
|
526
|
+
const searchInput = new Input();
|
|
527
|
+
searchInput.setValue("");
|
|
528
|
+
this.currentInput = searchInput;
|
|
529
|
+
this._searchInput = searchInput;
|
|
530
|
+
this._searchFocus = true;
|
|
531
|
+
|
|
532
|
+
const allItems = this.buildPresetCommandItemsWithOther();
|
|
533
|
+
const list = new SelectList(allItems, MAX_VISIBLE, this.theme);
|
|
534
|
+
this.currentList = list;
|
|
535
|
+
this._commandList = list;
|
|
536
|
+
|
|
537
|
+
searchInput.onSubmit = (val: string): void => {
|
|
538
|
+
const filtered = this._filterCommands(allItems, val.trim());
|
|
539
|
+
if (filtered.length === 0 || (filtered.length === 1 && filtered[0]?.value === "__other__")) {
|
|
540
|
+
this.startCommandInput("deny-cmd");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const firstMatch = filtered.find((i) => i.value !== "__other__");
|
|
544
|
+
if (firstMatch !== undefined) {
|
|
545
|
+
this.fillSelections.cmd = firstMatch.value;
|
|
546
|
+
this.denySubStage = "subcmd";
|
|
547
|
+
this.startFillDenyFamily();
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
searchInput.onEscape = (): void => {
|
|
551
|
+
this.switchToListStage();
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
list.onSelect = (item: SelectItem): void => {
|
|
555
|
+
if (item.value === "__other__") {
|
|
556
|
+
this.startCommandInput("deny-cmd");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.fillSelections.cmd = item.value;
|
|
560
|
+
this.denySubStage = "subcmd";
|
|
561
|
+
this.startFillDenyFamily();
|
|
562
|
+
};
|
|
563
|
+
list.onCancel = (): void => {
|
|
564
|
+
this.switchToListStage();
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
this.addChild(new TextLines(["[pi-permission] Deny — Select command", ""]));
|
|
568
|
+
this.addChild(new TextLines(["Type to search · Tab to switch focus · Enter to select"]));
|
|
569
|
+
this.addChild(searchInput);
|
|
570
|
+
this.addChild(list);
|
|
571
|
+
} else {
|
|
572
|
+
// subcmd 阶段
|
|
573
|
+
const items: SelectItem[] = [
|
|
574
|
+
{ value: "__any__", label: "[Any subcommand]", description: "Deny all subcommands of this command" },
|
|
575
|
+
{ value: "__specific__", label: "[Specific subcommand]", description: "Deny a specific subcommand" },
|
|
576
|
+
];
|
|
577
|
+
const list = new SelectList(items, MAX_VISIBLE, this.theme);
|
|
578
|
+
list.onSelect = (item: SelectItem): void => {
|
|
579
|
+
if (item.value === "__any__") {
|
|
580
|
+
this.fillSelections.subcmd = "__any__";
|
|
581
|
+
this.commitFill();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// specific → Input
|
|
585
|
+
this.startSubcmdInput();
|
|
586
|
+
};
|
|
587
|
+
list.onCancel = (): void => {
|
|
588
|
+
// 回到 cmd 选择
|
|
589
|
+
this.denySubStage = "cmd";
|
|
590
|
+
this.startFillDenyFamily();
|
|
591
|
+
};
|
|
592
|
+
this.currentList = list;
|
|
593
|
+
this.addChild(new TextLines([`[pi-permission] Deny ${this.fillSelections.cmd ?? "?"} — Select scope`, ""]));
|
|
594
|
+
this.addChild(list);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── allow-subcmd(命令 → Input 子命令)──
|
|
599
|
+
|
|
600
|
+
private startFillAllowSubcmd(): void {
|
|
601
|
+
this.stage = "fill";
|
|
602
|
+
this.fillKind = "allow-subcmd";
|
|
603
|
+
this.clear();
|
|
604
|
+
this._resetStageRefs();
|
|
605
|
+
|
|
606
|
+
// 带搜索的命令选择
|
|
607
|
+
const searchInput = new Input();
|
|
608
|
+
searchInput.setValue("");
|
|
609
|
+
this.currentInput = searchInput;
|
|
610
|
+
this._searchInput = searchInput;
|
|
611
|
+
this._searchFocus = true;
|
|
612
|
+
|
|
613
|
+
const allItems = this.buildPresetCommandItemsWithOther();
|
|
614
|
+
const list = new SelectList(allItems, MAX_VISIBLE, this.theme);
|
|
615
|
+
this.currentList = list;
|
|
616
|
+
this._commandList = list;
|
|
617
|
+
|
|
618
|
+
searchInput.onSubmit = (val: string): void => {
|
|
619
|
+
const filtered = this._filterCommands(allItems, val.trim());
|
|
620
|
+
if (filtered.length === 0 || (filtered.length === 1 && filtered[0]?.value === "__other__")) {
|
|
621
|
+
this.startCommandInput("allow-subcmd-cmd");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const firstMatch = filtered.find((i) => i.value !== "__other__");
|
|
625
|
+
if (firstMatch !== undefined) {
|
|
626
|
+
this.fillSelections.cmd = firstMatch.value;
|
|
627
|
+
this.startSubcmdInput();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
searchInput.onEscape = (): void => {
|
|
631
|
+
this.switchToListStage();
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
list.onSelect = (item: SelectItem): void => {
|
|
635
|
+
if (item.value === "__other__") {
|
|
636
|
+
this.startCommandInput("allow-subcmd-cmd");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
this.fillSelections.cmd = item.value;
|
|
640
|
+
this.startSubcmdInput();
|
|
641
|
+
};
|
|
642
|
+
list.onCancel = (): void => {
|
|
643
|
+
this.switchToListStage();
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
this.addChild(new TextLines(["[pi-permission] Allow subcommand — Select command", ""]));
|
|
647
|
+
this.addChild(new TextLines(["Type to search · Tab to switch focus · Enter to select"]));
|
|
648
|
+
this.addChild(searchInput);
|
|
649
|
+
this.addChild(list);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── custom form(5 项 Tab 焦点,WR8)──
|
|
653
|
+
|
|
654
|
+
private startFillCustom(): void {
|
|
655
|
+
this.stage = "fill";
|
|
656
|
+
this.fillKind = "custom";
|
|
657
|
+
this.clear();
|
|
658
|
+
this._resetStageRefs();
|
|
659
|
+
this.customFocusIndex = 0;
|
|
660
|
+
|
|
661
|
+
// 1. pattern Input
|
|
662
|
+
const patternInput = new Input();
|
|
663
|
+
patternInput.setValue(this.fillSelections.pattern ?? "");
|
|
664
|
+
patternInput.onSubmit = (val: string): void => {
|
|
665
|
+
this.fillSelections.pattern = val;
|
|
666
|
+
};
|
|
667
|
+
patternInput.onEscape = (): void => {
|
|
668
|
+
this.switchToListStage();
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// 2. action SelectList
|
|
672
|
+
const actionItems: SelectItem[] = [
|
|
673
|
+
{ value: "allow", label: "allow" },
|
|
674
|
+
{ value: "deny", label: "deny" },
|
|
675
|
+
{ value: "ask", label: "ask" },
|
|
676
|
+
];
|
|
677
|
+
const actionList = new SelectList(actionItems, 3, this.theme);
|
|
678
|
+
actionList.onSelect = (item: SelectItem): void => {
|
|
679
|
+
this.fillSelections.action = item.value as PermissionAction;
|
|
680
|
+
};
|
|
681
|
+
actionList.onCancel = (): void => {
|
|
682
|
+
this.switchToListStage();
|
|
683
|
+
};
|
|
684
|
+
// 预选当前 action
|
|
685
|
+
if (this.fillSelections.action !== undefined) {
|
|
686
|
+
const idx = actionItems.findIndex((a) => a.value === this.fillSelections.action);
|
|
687
|
+
if (idx >= 0) actionList.setSelectedIndex(idx);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// 3. tool SelectList(G9:只列 bash)
|
|
691
|
+
const toolItems: SelectItem[] = [{ value: "bash", label: "bash" }];
|
|
692
|
+
const toolList = new SelectList(toolItems, 1, this.theme);
|
|
693
|
+
toolList.onSelect = (item: SelectItem): void => {
|
|
694
|
+
this.fillSelections.tool = item.value;
|
|
695
|
+
};
|
|
696
|
+
toolList.onCancel = (): void => {
|
|
697
|
+
this.switchToListStage();
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// 4. description Input
|
|
701
|
+
const descInput = new Input();
|
|
702
|
+
descInput.setValue(this.fillSelections.description ?? "");
|
|
703
|
+
descInput.onSubmit = (val: string): void => {
|
|
704
|
+
this.fillSelections.description = val || undefined;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// 5. Submit SelectList
|
|
708
|
+
const submitItems: SelectItem[] = [
|
|
709
|
+
{ value: "__submit__", label: "[Submit]" },
|
|
710
|
+
{ value: "__cancel__", label: "[Cancel]" },
|
|
711
|
+
];
|
|
712
|
+
const submitList = new SelectList(submitItems, 2, this.theme);
|
|
713
|
+
submitList.onSelect = (item: SelectItem): void => {
|
|
714
|
+
if (item.value === "__submit__") {
|
|
715
|
+
this.commitFill();
|
|
716
|
+
} else {
|
|
717
|
+
this.switchToListStage();
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
submitList.onCancel = (): void => {
|
|
721
|
+
this.switchToListStage();
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
this.customChildren = [patternInput, actionList, toolList, descInput, submitList];
|
|
725
|
+
|
|
726
|
+
// 组装 UI
|
|
727
|
+
const title = this.fillEditMode ? "Edit Rule (Tab to switch fields)" : "Custom Rule (Tab to switch fields)";
|
|
728
|
+
this.addChild(new TextLines([`[pi-permission] ${title}`, ""]));
|
|
729
|
+
this.addChild(new TextLines(["Pattern (wildcard, e.g. 'npm *')"]));
|
|
730
|
+
this.addChild(patternInput);
|
|
731
|
+
this.addChild(new TextLines(["Action"]));
|
|
732
|
+
this.addChild(actionList);
|
|
733
|
+
this.addChild(new TextLines(["Tool"]));
|
|
734
|
+
this.addChild(toolList);
|
|
735
|
+
this.addChild(new TextLines(["Description (optional)"]));
|
|
736
|
+
this.addChild(descInput);
|
|
737
|
+
this.addChild(new TextLines([""]));
|
|
738
|
+
this.addChild(submitList);
|
|
739
|
+
|
|
740
|
+
// edit 模式:底部加 [Delete this rule]
|
|
741
|
+
if (this.fillEditMode && this.fillEditRuleId !== null) {
|
|
742
|
+
const deleteItems: SelectItem[] = [{ value: "__delete__", label: "[Delete this rule]" }];
|
|
743
|
+
const deleteList = new SelectList(deleteItems, 1, this.theme);
|
|
744
|
+
deleteList.onSelect = (): void => {
|
|
745
|
+
this.ops.push({ kind: "delete", id: this.fillEditRuleId! });
|
|
746
|
+
this.switchToListStage();
|
|
747
|
+
};
|
|
748
|
+
deleteList.onCancel = (): void => {
|
|
749
|
+
this.switchToListStage();
|
|
750
|
+
};
|
|
751
|
+
this.addChild(new TextLines([""]));
|
|
752
|
+
this.addChild(deleteList);
|
|
753
|
+
// 添加到 customChildren 以便 Tab 切换
|
|
754
|
+
this.customChildren.push(deleteList);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
this.syncCustomFocus();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── command Input(Other → 手动输入命令名)──
|
|
761
|
+
|
|
762
|
+
private startCommandInput(returnTo?: string): void {
|
|
763
|
+
this.clear();
|
|
764
|
+
this._resetStageRefs();
|
|
765
|
+
const input = new Input();
|
|
766
|
+
input.focused = true;
|
|
767
|
+
input.onSubmit = (val: string): void => {
|
|
768
|
+
const cmd = val.trim();
|
|
769
|
+
if (cmd.length > 0) {
|
|
770
|
+
this.fillSelections.cmd = cmd;
|
|
771
|
+
if (returnTo === "deny-cmd") {
|
|
772
|
+
this.denySubStage = "subcmd";
|
|
773
|
+
this.startFillDenyFamily();
|
|
774
|
+
} else if (returnTo === "allow-subcmd-cmd") {
|
|
775
|
+
this.startSubcmdInput();
|
|
776
|
+
} else {
|
|
777
|
+
this.commitFill();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
input.onEscape = (): void => {
|
|
782
|
+
this.switchToListStage();
|
|
783
|
+
};
|
|
784
|
+
this.currentInput = input;
|
|
785
|
+
this.addChild(new TextLines(["[pi-permission] Enter command name", ""]));
|
|
786
|
+
this.addChild(input);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ── subcmd Input ──
|
|
790
|
+
|
|
791
|
+
private startSubcmdInput(): void {
|
|
792
|
+
this.clear();
|
|
793
|
+
this._resetStageRefs();
|
|
794
|
+
const input = new Input();
|
|
795
|
+
input.focused = true;
|
|
796
|
+
input.onSubmit = (val: string): void => {
|
|
797
|
+
const subcmd = val.trim();
|
|
798
|
+
if (subcmd.length > 0) {
|
|
799
|
+
this.fillSelections.subcmd = subcmd;
|
|
800
|
+
this.commitFill();
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
input.onEscape = (): void => {
|
|
804
|
+
this.switchToListStage();
|
|
805
|
+
};
|
|
806
|
+
this.currentInput = input;
|
|
807
|
+
this.addChild(new TextLines(["[pi-permission] Enter subcommand name", ""]));
|
|
808
|
+
this.addChild(input);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ──────────────────────── commitFill ────────────────────────
|
|
812
|
+
|
|
813
|
+
/** 提交当前 fill 结果:build rule + 赋 id + push op。 */
|
|
814
|
+
private commitFill(): void {
|
|
815
|
+
this._syncCustomFormValues(); // 兜底刷新 Input 值(M2 修复)
|
|
816
|
+
|
|
817
|
+
// edit 模式:直接构造 Rule,绕过 fillTemplate.build(M1 修复)。
|
|
818
|
+
// enterFillEditMode 走 custom form 但从不设置 fillTemplate,故 fillTemplate 为 null,
|
|
819
|
+
// 旧逻辑会在 fillTemplate === null 时提前 switchToListStage(),导致 edit 永不落盘。
|
|
820
|
+
if (this.fillEditMode && this.fillEditRuleId !== null) {
|
|
821
|
+
const pattern = (this.fillSelections.pattern && this.fillSelections.pattern.trim()) || "*";
|
|
822
|
+
const rule: Rule = {
|
|
823
|
+
id: this.fillEditRuleId,
|
|
824
|
+
pattern,
|
|
825
|
+
action: this.fillSelections.action ?? "ask",
|
|
826
|
+
tool: this.fillSelections.tool ?? "bash",
|
|
827
|
+
source: "user",
|
|
828
|
+
description: this.fillSelections.description,
|
|
829
|
+
};
|
|
830
|
+
this.ops.push({ kind: "edit", id: this.fillEditRuleId, rule });
|
|
831
|
+
this.switchToListStage();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// new 模式:走 fillTemplate.build
|
|
836
|
+
if (this.fillTemplate === null) {
|
|
837
|
+
this.switchToListStage();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const built = this.fillTemplate.build(this.fillSelections);
|
|
841
|
+
const ruleId = this.sessionIdCounter(); // G13:立即赋值
|
|
842
|
+
const rule: Rule = {
|
|
843
|
+
id: ruleId,
|
|
844
|
+
...built,
|
|
845
|
+
};
|
|
846
|
+
this.ops.push({ kind: "add", rule });
|
|
847
|
+
this.switchToListStage();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* 兜底同步 custom form 的 Input 值到 fillSelections(M2 修复)。
|
|
852
|
+
*
|
|
853
|
+
* pi-tui Input.onSubmit 仅在 Enter 时触发,用户按 Tab 切换焦点不会触发 onSubmit。
|
|
854
|
+
* 本方法在 commitFill 入口调用,确保 Tab 后 Submit 的数据不丢失。
|
|
855
|
+
*
|
|
856
|
+
* customChildren index 约定(见 startFillCustom):
|
|
857
|
+
* [0]=patternInput, [1]=actionList, [2]=toolList, [3]=descInput, [4]=submitList[, 5]=deleteList
|
|
858
|
+
* SelectList(action/tool)的 onSelect 已实时更新 fillSelections,无需在此同步。
|
|
859
|
+
*/
|
|
860
|
+
private _syncCustomFormValues(): void {
|
|
861
|
+
const patternInput = this.customChildren[0];
|
|
862
|
+
if (patternInput instanceof Input) {
|
|
863
|
+
this.fillSelections.pattern = patternInput.getValue();
|
|
864
|
+
}
|
|
865
|
+
const descInput = this.customChildren[3];
|
|
866
|
+
if (descInput instanceof Input) {
|
|
867
|
+
const desc = descInput.getValue();
|
|
868
|
+
this.fillSelections.description = desc || undefined;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ──────────────────────── 辅助 ────────────────────────
|
|
873
|
+
|
|
874
|
+
/** 同步 Custom form 焦点标志(Input.focused + rerender)。 */
|
|
875
|
+
private syncCustomFocus(): void {
|
|
876
|
+
for (let i = 0; i < this.customChildren.length; i++) {
|
|
877
|
+
const child = this.customChildren[i];
|
|
878
|
+
if (child !== undefined && "focused" in child) {
|
|
879
|
+
(child as unknown as { focused: boolean }).focused = i === this.customFocusIndex;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|