@hank-warren/pi-auto-permissions 0.2.0 → 0.3.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/NOTICE.md +1 -1
- package/README.md +7 -1
- package/index.ts +50 -23
- package/package.json +2 -1
- package/widget-status.ts +89 -0
package/NOTICE.md
CHANGED
|
@@ -4,4 +4,4 @@ This package is a fork of [`@ogulcancelik/pi-auto-permissions`](https://github.c
|
|
|
4
4
|
|
|
5
5
|
The original work is Copyright (c) 2026 Can Celik and is used under the MIT License included in [`LICENSE`](LICENSE).
|
|
6
6
|
|
|
7
|
-
Fork-specific changes are Copyright (c) 2026 Hank Warren and are released under the same MIT License. They include the `reviewEvidence.userAnswerTools` dialog-answer evidence feature, proposed upstream in [ogulcancelik/pi-extensions#24](https://github.com/ogulcancelik/pi-extensions/issues/24) / [PR #25](https://github.com/ogulcancelik/pi-extensions/pull/25),
|
|
7
|
+
Fork-specific changes are Copyright (c) 2026 Hank Warren and are released under the same MIT License. They include the `reviewEvidence.userAnswerTools` dialog-answer evidence feature, proposed upstream in [ogulcancelik/pi-extensions#24](https://github.com/ogulcancelik/pi-extensions/issues/24) / [PR #25](https://github.com/ogulcancelik/pi-extensions/pull/25), the prompted-review evaluation log with explicit user feedback, and the single-line animated review status widget.
|
package/README.md
CHANGED
|
@@ -235,7 +235,13 @@ The path defaults to `usage.jsonl` beside the Auto Permissions config and resolv
|
|
|
235
235
|
|
|
236
236
|
## Review display
|
|
237
237
|
|
|
238
|
-
The default UI shows guardian progress in a temporary widget below the editor
|
|
238
|
+
The default UI shows guardian progress as a single animated status line in a temporary widget below the editor:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
auto permissions · Git commit · ✶ waiting for openai-codex/gpt-5.6-luna
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A sparkle spinner (`✶ ✸ ✻ ✽`) cycles while the guardian is reviewing and resolves to `✓ approved`, `↻ revision requested`, `? waiting for your approval`, or `✗ blocked`. The guardian's reason, when present, appears on a dim second line; the command itself is not repeated because it is already visible in the Bash tool box. Configure the widget with:
|
|
239
245
|
|
|
240
246
|
```json
|
|
241
247
|
{
|
package/index.ts
CHANGED
|
@@ -17,6 +17,13 @@ import {
|
|
|
17
17
|
} from "./evaluation-log.js";
|
|
18
18
|
import { findGates, type Gate } from "./gates.js";
|
|
19
19
|
import { appendUsageRecord, buildUsageLogRecord } from "./usage-log.js";
|
|
20
|
+
import {
|
|
21
|
+
reviewFrameIntervalMs,
|
|
22
|
+
reviewStatusFrame,
|
|
23
|
+
reviewStatusLines,
|
|
24
|
+
type ReviewDisplayState,
|
|
25
|
+
type ReviewLinePalette,
|
|
26
|
+
} from "./widget-status.js";
|
|
20
27
|
import {
|
|
21
28
|
buildReviewEnvelope,
|
|
22
29
|
collectReviewEvidence,
|
|
@@ -29,7 +36,6 @@ const WIDGET_KEY = "auto-permissions";
|
|
|
29
36
|
const PROJECT_CONFIG_DIR_NAME = (PiCodingAgent as { CONFIG_DIR_NAME?: string }).CONFIG_DIR_NAME ?? ".pi";
|
|
30
37
|
|
|
31
38
|
type BlockResult = { block: true; reason: string };
|
|
32
|
-
type ReviewDisplayState = "waiting" | "approved" | "revise" | "ask_user" | "blocked";
|
|
33
39
|
type ReviewTarget = { toolName: string; toolCallId?: string };
|
|
34
40
|
type ReviewRow = {
|
|
35
41
|
gate: Gate;
|
|
@@ -204,6 +210,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
204
210
|
let bashRendererRegistered = false;
|
|
205
211
|
let bashRendererSourcePath: string | undefined;
|
|
206
212
|
let clearWidgetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
213
|
+
let widgetAnimTimer: ReturnType<typeof setInterval> | undefined;
|
|
207
214
|
const reviewRows = new Map<string, ReviewRow>();
|
|
208
215
|
const reviewRowInvalidators = new Map<string, () => void>();
|
|
209
216
|
let reviewerLineage: ReviewerLineage | undefined;
|
|
@@ -473,6 +480,8 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
473
480
|
function clearReviewWidget(ctx: ExtensionContext): void {
|
|
474
481
|
if (clearWidgetTimer) clearTimeout(clearWidgetTimer);
|
|
475
482
|
clearWidgetTimer = undefined;
|
|
483
|
+
if (widgetAnimTimer) clearInterval(widgetAnimTimer);
|
|
484
|
+
widgetAnimTimer = undefined;
|
|
476
485
|
if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
477
486
|
}
|
|
478
487
|
|
|
@@ -491,7 +500,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
491
500
|
ctx: ExtensionContext,
|
|
492
501
|
config: AutoPermissionsConfig,
|
|
493
502
|
gate: Gate,
|
|
494
|
-
|
|
503
|
+
_command: string,
|
|
495
504
|
target: ReviewTarget,
|
|
496
505
|
state: ReviewDisplayState,
|
|
497
506
|
detail?: string,
|
|
@@ -512,27 +521,45 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
512
521
|
|
|
513
522
|
if (clearWidgetTimer) clearTimeout(clearWidgetTimer);
|
|
514
523
|
clearWidgetTimer = undefined;
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
524
|
+
if (widgetAnimTimer) clearInterval(widgetAnimTimer);
|
|
525
|
+
widgetAnimTimer = undefined;
|
|
526
|
+
ctx.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
527
|
+
const palette: ReviewLinePalette = {
|
|
528
|
+
header: (text) => theme.fg("accent", theme.bold(text)),
|
|
529
|
+
muted: (text) => theme.fg("muted", text),
|
|
530
|
+
warning: (text) => theme.fg("warning", text),
|
|
531
|
+
success: (text) => theme.fg("success", text),
|
|
532
|
+
accent: (text) => theme.fg("accent", text),
|
|
533
|
+
error: (text) => theme.fg("error", text),
|
|
534
|
+
};
|
|
535
|
+
let frameIndex = 0;
|
|
536
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
537
|
+
const stopTimer = () => {
|
|
538
|
+
if (timer) clearInterval(timer);
|
|
539
|
+
if (widgetAnimTimer === timer) widgetAnimTimer = undefined;
|
|
540
|
+
timer = undefined;
|
|
541
|
+
};
|
|
542
|
+
const intervalMs = reviewFrameIntervalMs(state);
|
|
543
|
+
if (intervalMs !== undefined) {
|
|
544
|
+
timer = setInterval(() => {
|
|
545
|
+
frameIndex++;
|
|
546
|
+
if (reviewStatusFrame(state, reviewer, frameIndex).done) stopTimer();
|
|
547
|
+
tui.requestRender();
|
|
548
|
+
}, intervalMs);
|
|
549
|
+
timer.unref?.();
|
|
550
|
+
widgetAnimTimer = timer;
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
render(width: number): string[] {
|
|
554
|
+
return reviewStatusLines(state, gate.label, reviewer, frameIndex, detail, palette)
|
|
555
|
+
.map((line) => truncateToWidth(line, Math.max(1, width)));
|
|
556
|
+
},
|
|
557
|
+
invalidate() {},
|
|
558
|
+
dispose() {
|
|
559
|
+
stopTimer();
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
}, { placement: "belowEditor" });
|
|
536
563
|
|
|
537
564
|
if (autoClear) {
|
|
538
565
|
clearWidgetTimer = setTimeout(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-auto-permissions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Fork of @ogulcancelik/pi-auto-permissions with dialog-answer user evidence: context-aware Bash permissions for Pi with automated guardian review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"gates.ts",
|
|
45
45
|
"review.ts",
|
|
46
46
|
"usage-log.ts",
|
|
47
|
+
"widget-status.ts",
|
|
47
48
|
"README.md",
|
|
48
49
|
"LICENSE"
|
|
49
50
|
],
|
package/widget-status.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the below-editor review status widget: single-line status
|
|
3
|
+
* formatting plus frame progression for the waiting sparkle spinner. Kept
|
|
4
|
+
* free of TUI dependencies so they are unit-testable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ReviewDisplayState = "waiting" | "approved" | "revise" | "ask_user" | "blocked";
|
|
8
|
+
|
|
9
|
+
export type ReviewStatusTone = "warning" | "success" | "accent" | "error";
|
|
10
|
+
|
|
11
|
+
export interface ReviewStatusFrame {
|
|
12
|
+
glyph: string;
|
|
13
|
+
label: string;
|
|
14
|
+
tone: ReviewStatusTone;
|
|
15
|
+
/** True once no further frames are needed (state is static). */
|
|
16
|
+
done: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const WAITING_FRAMES = ["✶", "✸", "✻", "✽"] as const;
|
|
20
|
+
export const WAITING_FRAME_INTERVAL_MS = 120;
|
|
21
|
+
|
|
22
|
+
export function reviewStatusFrame(
|
|
23
|
+
state: ReviewDisplayState,
|
|
24
|
+
reviewer: string,
|
|
25
|
+
frameIndex: number,
|
|
26
|
+
): ReviewStatusFrame {
|
|
27
|
+
const index = Math.max(0, Math.floor(frameIndex));
|
|
28
|
+
switch (state) {
|
|
29
|
+
case "waiting":
|
|
30
|
+
return {
|
|
31
|
+
glyph: WAITING_FRAMES[index % WAITING_FRAMES.length],
|
|
32
|
+
label: `waiting for ${reviewer}`,
|
|
33
|
+
tone: "warning",
|
|
34
|
+
done: false,
|
|
35
|
+
};
|
|
36
|
+
case "approved":
|
|
37
|
+
return { glyph: "✓", label: "approved", tone: "success", done: true };
|
|
38
|
+
case "revise":
|
|
39
|
+
return { glyph: "↻", label: "revision requested", tone: "warning", done: true };
|
|
40
|
+
case "ask_user":
|
|
41
|
+
return { glyph: "?", label: "waiting for your approval", tone: "accent", done: true };
|
|
42
|
+
case "blocked":
|
|
43
|
+
return { glyph: "✗", label: "blocked", tone: "error", done: true };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function reviewFrameIntervalMs(state: ReviewDisplayState): number | undefined {
|
|
48
|
+
return state === "waiting" ? WAITING_FRAME_INTERVAL_MS : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReviewLinePalette {
|
|
52
|
+
header: (text: string) => string;
|
|
53
|
+
muted: (text: string) => string;
|
|
54
|
+
warning: (text: string) => string;
|
|
55
|
+
success: (text: string) => string;
|
|
56
|
+
accent: (text: string) => string;
|
|
57
|
+
error: (text: string) => string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const PLAIN_PALETTE: ReviewLinePalette = {
|
|
61
|
+
header: (text) => text,
|
|
62
|
+
muted: (text) => text,
|
|
63
|
+
warning: (text) => text,
|
|
64
|
+
success: (text) => text,
|
|
65
|
+
accent: (text) => text,
|
|
66
|
+
error: (text) => text,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render the widget content: one status line, plus a dim detail line only
|
|
71
|
+
* when a guardian reason is present. The command is intentionally omitted —
|
|
72
|
+
* it is already visible in the bash tool box above.
|
|
73
|
+
*/
|
|
74
|
+
export function reviewStatusLines(
|
|
75
|
+
state: ReviewDisplayState,
|
|
76
|
+
gateLabel: string,
|
|
77
|
+
reviewer: string,
|
|
78
|
+
frameIndex: number,
|
|
79
|
+
detail: string | undefined,
|
|
80
|
+
palette: ReviewLinePalette,
|
|
81
|
+
): string[] {
|
|
82
|
+
const frame = reviewStatusFrame(state, reviewer, frameIndex);
|
|
83
|
+
const status = palette[frame.tone](`${frame.glyph} ${frame.label}`);
|
|
84
|
+
const lines = [
|
|
85
|
+
`${palette.header("auto permissions")} ${palette.muted(`· ${gateLabel} ·`)} ${status}`,
|
|
86
|
+
];
|
|
87
|
+
if (detail) lines.push(palette.muted(` ${detail}`));
|
|
88
|
+
return lines;
|
|
89
|
+
}
|