@trim21/personal-pi-extensions 0.0.295 → 0.0.297

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.295",
3
+ "version": "0.0.297",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -16,7 +16,12 @@ import {
16
16
  import { type TObject, Type } from "typebox";
17
17
 
18
18
  import { type CommandSpec, parseCommand } from "../lib/cli.js";
19
- import { type SelectAction, selectWithOptionalInput } from "../lib/ui.js";
19
+ import {
20
+ type CheckboxAction,
21
+ type SelectAction,
22
+ selectCheckboxActions,
23
+ selectWithOptionalInput,
24
+ } from "../lib/ui.js";
20
25
  import { type ApprovalRule, commandPatternsFor, evaluateBashApproval } from "./approval-rules.js";
21
26
  import {
22
27
  type BwrapMode,
@@ -412,8 +417,9 @@ export class BwrapRuntime {
412
417
  ): Promise<void> {
413
418
  const policy = resolveEscalation({ hasUI: ctx.hasUI });
414
419
  if (policy.kind === "deny") throw new Error(policy.reason);
415
- // 弹框前解析命令的持久化规则(Allow forever 会写入),在弹框里预览给
416
- // 用户:`echo 1` → `echo *`,避免用户对"永久允许"持久化什么一无所知。
420
+ // 弹框前解析命令的持久化规则(勾选的 pattern 会写入),在弹框里以
421
+ // checkbox 列出:`echo 1 | head` → `echo *`、`head *`,逐项决定是否
422
+ // allow forever,避免用户对"永久允许"持久化什么一无所知。
417
423
  const patterns = await commandPatternsFor(command);
418
424
  // dcg 扫描建议是可选的参考文本:未安装时静默跳过;已安装但扫描失败
419
425
  // 时 notify 提示,弹窗本身与无 dcg 时一致
@@ -421,7 +427,7 @@ export class BwrapRuntime {
421
427
  if (outcome.kind === "failed") {
422
428
  ctx.ui.notify(`dcg 扫描失败,本次无破坏性命令建议: ${outcome.detail}`, "warning");
423
429
  }
424
- // 弹框主体按行组织('\n' join),便于 review;suggestion 与 forever 预览
430
+ // 弹框主体按行组织('\n' join),便于 review;suggestion 与规则说明
425
431
  // 块带前导空行 + 尾部 "---" 分隔,输出与历史逐字符一致。
426
432
  const lines: string[] = [
427
433
  "Allow this command to run without sandbox?",
@@ -434,36 +440,78 @@ export class BwrapRuntime {
434
440
  lines.push("", outcome.suggestion.text, "---");
435
441
  }
436
442
  if (patterns.length > 0) {
437
- lines.push(
438
- "",
439
- `Allow forever 将持久化规则: ${patterns.map((pattern) => `\`${pattern}\``).join(", ")}`,
440
- "---",
441
- );
443
+ lines.push("", "勾选规则将永久允许(allow forever),未勾选规则仅本次放行:", "---");
442
444
  }
443
445
  lines.push(fenceCodeBlock(command));
444
446
  const description = lines.join("\n");
445
447
 
446
- // 单选:允许一次 / 永久允许(写入规则)/ 拒绝 / 拒绝并附理由(弹输入框)
447
- const verdict = await selectWithOptionalInput(description, FULL_ACCESS_CHOICES, ctx.ui, {
448
- signal: ctx.signal,
449
- });
448
+ // 解析失败(无 pattern 可勾选):保持单选对话框(Allow forever 是空操作)
449
+ if (patterns.length === 0) {
450
+ // 单选:允许一次 / 永久允许(写入规则)/ 拒绝 / 拒绝并附理由(弹输入框)
451
+ const verdict = await selectWithOptionalInput(description, FULL_ACCESS_CHOICES, ctx.ui, {
452
+ signal: ctx.signal,
453
+ });
454
+ // 关闭对话框 = 中断并拒绝,不循环重问
455
+ if (verdict === undefined) {
456
+ ctx.abort();
457
+ throw new Error("User denied the command execution.");
458
+ }
459
+ switch (verdict.label) {
460
+ case ALLOW_ONCE: {
461
+ return;
462
+ }
463
+ case ALLOW_FOREVER: {
464
+ await this.persistAllowRule(ctx, command, patterns);
465
+ return;
466
+ }
467
+ case DENY: {
468
+ throw new Error("User denied unsandboxed execution.");
469
+ }
470
+ case DENY_WITH_REASON: {
471
+ const feedback = verdict.input?.trim() ?? "";
472
+ throw new Error(
473
+ feedback
474
+ ? `User denied unsandboxed execution: ${feedback}`
475
+ : "User denied unsandboxed execution.",
476
+ );
477
+ }
478
+ }
479
+ }
480
+
481
+ // 每个识别到的 pattern 一个 checkbox:勾选 = 该规则 allow forever。
482
+ // Allow once = 执行本次并持久化勾选的规则;Deny 系列不持久化任何规则。
483
+ const actions = [
484
+ { action: "allow-once", label: ALLOW_ONCE },
485
+ { action: "deny", label: DENY },
486
+ {
487
+ action: "deny-with-reason",
488
+ label: DENY_WITH_REASON,
489
+ inputPrompt: "Why was this denied?",
490
+ },
491
+ ] as const satisfies readonly CheckboxAction<"allow-once" | "deny" | "deny-with-reason">[];
492
+ const verdict = await selectCheckboxActions(
493
+ description,
494
+ [...new Set(patterns)].map((pattern) => ({ label: pattern })),
495
+ actions,
496
+ ctx.ui,
497
+ { signal: ctx.signal },
498
+ );
450
499
  // 关闭对话框 = 中断并拒绝,不循环重问
451
500
  if (verdict === undefined) {
452
501
  ctx.abort();
453
502
  throw new Error("User denied the command execution.");
454
503
  }
455
- switch (verdict.label) {
456
- case ALLOW_ONCE: {
457
- return;
458
- }
459
- case ALLOW_FOREVER: {
460
- await this.persistAllowRule(ctx, command, patterns);
504
+ switch (verdict.action) {
505
+ case "allow-once": {
506
+ if (verdict.selected.length > 0) {
507
+ await this.persistAllowRule(ctx, command, verdict.selected);
508
+ }
461
509
  return;
462
510
  }
463
- case DENY: {
511
+ case "deny": {
464
512
  throw new Error("User denied unsandboxed execution.");
465
513
  }
466
- case DENY_WITH_REASON: {
514
+ case "deny-with-reason": {
467
515
  const feedback = verdict.input?.trim() ?? "";
468
516
  throw new Error(
469
517
  feedback
package/src/lib/ui.ts CHANGED
@@ -111,3 +111,54 @@ export async function selectMultiple(
111
111
  }
112
112
  return selected;
113
113
  }
114
+
115
+ /** 结束动作:选中即结束多选循环;带 inputPrompt 时先弹输入框,内容经 input 返回。 */
116
+ export interface CheckboxAction<T extends string> {
117
+ action: T;
118
+ label: string;
119
+ inputPrompt?: string;
120
+ }
121
+
122
+ export interface CheckboxActionResult<T extends string> {
123
+ /** 勾选的条目 label(勾选顺序,经 inputPrompt 输入的条目已并入)。 */
124
+ selected: string[];
125
+ /** 用户选中的结束动作。 */
126
+ action: T;
127
+ /** 结束动作带 inputPrompt 时的输入内容:undefined=取消输入,""=空提交。 */
128
+ input?: string;
129
+ }
130
+
131
+ /**
132
+ * Checkbox 多选 + 结束动作的组合对话框(selectMultiple 的带操作变体)。
133
+ *
134
+ * 循环列出全部 `actions`(固定操作,选中即结束循环)与全部 `entries`
135
+ * (☑/☐ 前缀切换勾选);对话框关闭(dismiss)返回 undefined。操作在前、
136
+ * checkbox 条目在后,用户先决定本次动作,需要时再勾选要持久化的规则。
137
+ */
138
+ export async function selectCheckboxActions<T extends string>(
139
+ title: string,
140
+ entries: readonly SelectAction[],
141
+ actions: readonly CheckboxAction<T>[],
142
+ ui: ExtensionContext["ui"],
143
+ opts: { signal?: AbortSignal } = {},
144
+ ): Promise<CheckboxActionResult<T> | undefined> {
145
+ const selected: string[] = [];
146
+ while (true) {
147
+ const selectedSet = new Set(selected);
148
+ const displayToLabel = new Map<string, string>();
149
+ const round: SelectAction[] = [];
150
+ for (const entry of entries) {
151
+ const display = `${selectedSet.has(entry.label) ? CHECKED_PREFIX : UNCHECKED_PREFIX}${entry.label}`;
152
+ displayToLabel.set(display, entry.label);
153
+ round.push({ ...entry, label: display });
154
+ }
155
+ const result = await selectWithOptionalInput(title, [...actions, ...round], ui, opts);
156
+ if (result === undefined) return undefined;
157
+ const action = actions.find((candidate) => candidate.label === result.label);
158
+ if (action !== undefined) return { selected, action: action.action, input: result.input };
159
+ const label = displayToLabel.get(result.label);
160
+ if (label === undefined) continue;
161
+ if (selectedSet.has(label)) selected.splice(selected.indexOf(label), 1);
162
+ else selected.push(label);
163
+ }
164
+ }