@erichll/pi-auto-review 0.17.0 → 0.18.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/CHANGELOG.md +22 -0
- package/README.md +6 -5
- package/package.json +2 -2
- package/src/user-feedback.ts +80 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.1 - 2026-09-11
|
|
4
|
+
|
|
5
|
+
- Animate the live `reviewing` label in the above-editor widget with a
|
|
6
|
+
left-to-right light sweep while the reviewer model is evaluating a boundary check.
|
|
7
|
+
- Paint shimmer frames at an 80ms interval cycling theme colors (`accent`,
|
|
8
|
+
`muted`, `dim`) without altering the label text length or widget layout width.
|
|
9
|
+
- Ensure the animation timer cleanly stops and disposes when the review phase
|
|
10
|
+
completes or the widget is dismissed.
|
|
11
|
+
- Export `USER_REVIEW_SWEEP_INTERVAL_MS` and `renderReviewingSweep` for testing
|
|
12
|
+
and custom TUI rendering.
|
|
13
|
+
|
|
14
|
+
## 0.18.0 - 2026-09-11
|
|
15
|
+
|
|
16
|
+
- Coordinated release for `@erichll/pi-sandbox 0.18.0`.
|
|
17
|
+
- Dismiss the above-editor widget eight seconds after an allow or auto-confirm
|
|
18
|
+
so a successful check does not stay on screen until the next review. Denials,
|
|
19
|
+
deferrals, and local-confirmation waits still remain until the next check.
|
|
20
|
+
- Raise the `@gotgenes/pi-permission-system` peer floor to `>=30.0.0` and drop
|
|
21
|
+
the upper bound. 29.x is no longer claimed; 32.x does not change the public
|
|
22
|
+
authorizer API this package uses, and later majors are no longer excluded by
|
|
23
|
+
the range.
|
|
24
|
+
|
|
3
25
|
## 0.17.0 - 2026-09-05
|
|
4
26
|
|
|
5
27
|
- Coordinated release for `@erichll/pi-sandbox 0.17.0`; the broker API and
|
package/README.md
CHANGED
|
@@ -31,8 +31,7 @@ that dependency is a hard prerequisite (see [Install and enable](#install-and-en
|
|
|
31
31
|
> **Prerequisite:** pi-auto-review is an authorizer inside
|
|
32
32
|
> `@gotgenes/pi-permission-system`. Pi does not auto-install peer packages, so
|
|
33
33
|
> install the permission system separately (once per machine) before this
|
|
34
|
-
> extension. This release line supports permission-system
|
|
35
|
-
> latest 31.x release:
|
|
34
|
+
> extension. This release line supports permission-system 30.0.0 and later:
|
|
36
35
|
|
|
37
36
|
Node.js 22.13.0 or newer is required. Permission auditing uses Node's built-in
|
|
38
37
|
`node:sqlite`; it does not require a SQLite CLI, system SQLite library, or npm
|
|
@@ -178,9 +177,11 @@ Interactive sessions show the current permission check in a single widget
|
|
|
178
177
|
above the editor. Each check first shows its surface, compact target, and the
|
|
179
178
|
dynamically configured reviewer model, then replaces that content in place
|
|
180
179
|
with the outcome, target and rationale, model, token usage, duration, and any
|
|
181
|
-
extra call count. A new check replaces the previous result
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
extra call count. A new check replaces the previous result. Allowed and
|
|
181
|
+
auto-confirmed results dismiss after eight seconds so they do not occupy the
|
|
182
|
+
editor; denials, deferrals, and local-confirmation waits stay until the next
|
|
183
|
+
check, a session change, or shutdown. Concurrent older checks cannot overwrite
|
|
184
|
+
the most recently started one.
|
|
184
185
|
|
|
185
186
|
Every request still has its own model call, verdict, grant, local confirmation,
|
|
186
187
|
and audit record. No new review-result transcript entries are written. Existing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-auto-review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Fail-closed, model-backed approval broker for Pi: deterministically hard-denies dangerous operations, reviews dangerous boundaries with a reviewer model, and issues one-shot expiring grants that OS sandbox adapters must consume before retrying.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@earendil-works/pi-ai": "^0.85.0",
|
|
51
51
|
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
52
|
-
"@gotgenes/pi-permission-system": ">=
|
|
52
|
+
"@gotgenes/pi-permission-system": ">=30.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@earendil-works/pi-ai": "^0.85.0",
|
package/src/user-feedback.ts
CHANGED
|
@@ -3,6 +3,10 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
export const USER_REVIEW_STATUS_KEY = "pi-auto-review";
|
|
4
4
|
export const USER_REVIEW_ENTRY_TYPE = "pi-auto-review";
|
|
5
5
|
export const USER_REVIEW_WIDGET_KEY = "pi-auto-review";
|
|
6
|
+
/** How long a successful completed widget stays above the editor. */
|
|
7
|
+
export const USER_REVIEW_SUCCESS_WIDGET_TTL_MS = 8_000;
|
|
8
|
+
/** Frame interval for the light sweep across the live `reviewing` label. */
|
|
9
|
+
export const USER_REVIEW_SWEEP_INTERVAL_MS = 80;
|
|
6
10
|
|
|
7
11
|
export type UserReviewOutcome =
|
|
8
12
|
| "allow"
|
|
@@ -598,14 +602,36 @@ export function buildUserReviewWidgetData(
|
|
|
598
602
|
};
|
|
599
603
|
}
|
|
600
604
|
|
|
605
|
+
const REVIEWING_LABEL = "reviewing";
|
|
606
|
+
const REVIEWING_SWEEP_GAP_FRAMES = 4;
|
|
607
|
+
|
|
608
|
+
/** Paint one left-to-right shimmer frame without changing the label width. */
|
|
609
|
+
export function renderReviewingSweep(
|
|
610
|
+
theme: UserReviewTheme,
|
|
611
|
+
frame: number,
|
|
612
|
+
): string {
|
|
613
|
+
const center = Math.abs(Math.trunc(frame)) %
|
|
614
|
+
(REVIEWING_LABEL.length + REVIEWING_SWEEP_GAP_FRAMES) - 2;
|
|
615
|
+
return [...REVIEWING_LABEL].map((character, index) => {
|
|
616
|
+
const distance = Math.abs(index - center);
|
|
617
|
+
const color = distance === 0
|
|
618
|
+
? "accent"
|
|
619
|
+
: distance === 1
|
|
620
|
+
? "muted"
|
|
621
|
+
: "dim";
|
|
622
|
+
return `${theme.getFgAnsi(color)}${character}`;
|
|
623
|
+
}).join("") + "\x1b[0m";
|
|
624
|
+
}
|
|
625
|
+
|
|
601
626
|
export function renderUserReviewWidgetLines(
|
|
602
627
|
data: UserReviewWidgetData,
|
|
603
628
|
theme: UserReviewTheme,
|
|
604
629
|
width: number,
|
|
630
|
+
reviewingFrame = 0,
|
|
605
631
|
): string[] {
|
|
606
632
|
const rendered: string[] = [];
|
|
607
633
|
const verb = data.phase === "reviewing"
|
|
608
|
-
?
|
|
634
|
+
? REVIEWING_LABEL
|
|
609
635
|
: data.phase === "waiting_user"
|
|
610
636
|
? "waiting"
|
|
611
637
|
: outcomeVerb(data.outcome);
|
|
@@ -618,9 +644,12 @@ export function renderUserReviewWidgetLines(
|
|
|
618
644
|
for (const visual of wrapReviewDisplayText(line, Math.max(1, width))) {
|
|
619
645
|
let painted = `${mutedAnsi}${visual}\x1b[0m`;
|
|
620
646
|
if (index === 0) {
|
|
647
|
+
const paintedVerb = data.phase === "reviewing"
|
|
648
|
+
? renderReviewingSweep(theme, reviewingFrame)
|
|
649
|
+
: `${verbAnsi}${verb}\x1b[0m`;
|
|
621
650
|
painted = painted.replace(
|
|
622
651
|
verb,
|
|
623
|
-
`${
|
|
652
|
+
`${paintedVerb}${mutedAnsi}`,
|
|
624
653
|
);
|
|
625
654
|
}
|
|
626
655
|
rendered.push(painted);
|
|
@@ -638,11 +667,24 @@ function setWidget(
|
|
|
638
667
|
ctx.ui.setWidget(
|
|
639
668
|
USER_REVIEW_WIDGET_KEY,
|
|
640
669
|
data
|
|
641
|
-
? (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
670
|
+
? (tui, theme) => {
|
|
671
|
+
let frame = 0;
|
|
672
|
+
const interval = data.phase === "reviewing"
|
|
673
|
+
? setInterval(() => {
|
|
674
|
+
frame++;
|
|
675
|
+
tui.requestRender();
|
|
676
|
+
}, USER_REVIEW_SWEEP_INTERVAL_MS)
|
|
677
|
+
: undefined;
|
|
678
|
+
interval?.unref?.();
|
|
679
|
+
return {
|
|
680
|
+
render: (width: number) =>
|
|
681
|
+
renderUserReviewWidgetLines(data, theme, width, frame),
|
|
682
|
+
invalidate() {},
|
|
683
|
+
dispose() {
|
|
684
|
+
if (interval !== undefined) clearInterval(interval);
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
646
688
|
: undefined,
|
|
647
689
|
{ placement: "aboveEditor" },
|
|
648
690
|
);
|
|
@@ -652,9 +694,14 @@ function setWidget(
|
|
|
652
694
|
}
|
|
653
695
|
}
|
|
654
696
|
|
|
697
|
+
function isTransientCompletedOutcome(outcome: UserReviewOutcome): boolean {
|
|
698
|
+
return outcome === "allow" || outcome === "auto_confirm";
|
|
699
|
+
}
|
|
700
|
+
|
|
655
701
|
/** Owns the single live widget and rejects stale concurrent completions. */
|
|
656
702
|
export class UserReviewWidgetController {
|
|
657
703
|
#generation = 0;
|
|
704
|
+
#dismissTimer: ReturnType<typeof setTimeout> | undefined;
|
|
658
705
|
/** Set while a `ctx.ui` prompt blocks the session (ui_prompt span). */
|
|
659
706
|
#waiting?: { kind?: unknown; title?: unknown };
|
|
660
707
|
#current?: {
|
|
@@ -664,6 +711,9 @@ export class UserReviewWidgetController {
|
|
|
664
711
|
data?: UserReviewWidgetData;
|
|
665
712
|
notice?: UserReviewNotice;
|
|
666
713
|
};
|
|
714
|
+
constructor(
|
|
715
|
+
private readonly successTtlMs = USER_REVIEW_SUCCESS_WIDGET_TTL_MS,
|
|
716
|
+
) {}
|
|
667
717
|
|
|
668
718
|
begin(
|
|
669
719
|
requestId: string,
|
|
@@ -671,6 +721,7 @@ export class UserReviewWidgetController {
|
|
|
671
721
|
input: { surface: string; target?: string; model?: string },
|
|
672
722
|
): number {
|
|
673
723
|
const generation = ++this.#generation;
|
|
724
|
+
this.#cancelDismiss();
|
|
674
725
|
this.#current = { requestId, generation, ctx, data: buildUserReviewingWidgetData(input) };
|
|
675
726
|
this.#render();
|
|
676
727
|
return generation;
|
|
@@ -715,8 +766,12 @@ export class UserReviewWidgetController {
|
|
|
715
766
|
return;
|
|
716
767
|
}
|
|
717
768
|
if (!this.#isCurrent(requestId, generation)) return;
|
|
769
|
+
this.#cancelDismiss();
|
|
718
770
|
this.#current = { requestId, generation, ctx, notice, data };
|
|
719
771
|
if (!setWidget(ctx, data)) notifyUserReview(ctx, notice);
|
|
772
|
+
if (isTransientCompletedOutcome(data.outcome)) {
|
|
773
|
+
this.#scheduleDismiss(generation, ctx);
|
|
774
|
+
}
|
|
720
775
|
}
|
|
721
776
|
|
|
722
777
|
permissionDecision(event: unknown): void {
|
|
@@ -738,18 +793,36 @@ export class UserReviewWidgetController {
|
|
|
738
793
|
type: current.notice?.type ?? data.type,
|
|
739
794
|
message: data.lines.join("\n"),
|
|
740
795
|
};
|
|
796
|
+
this.#cancelDismiss();
|
|
741
797
|
this.#current = { ...current, data, notice };
|
|
742
798
|
if (!setWidget(current.ctx, data)) notifyUserReview(current.ctx, notice);
|
|
743
799
|
}
|
|
744
800
|
|
|
745
801
|
clear(ctx?: ExtensionContext): void {
|
|
746
802
|
++this.#generation;
|
|
803
|
+
this.#cancelDismiss();
|
|
747
804
|
this.#waiting = undefined;
|
|
748
805
|
const current = this.#current;
|
|
749
806
|
this.#current = undefined;
|
|
750
807
|
if (current) setWidget(ctx ?? current.ctx, undefined);
|
|
751
808
|
}
|
|
752
809
|
|
|
810
|
+
#cancelDismiss(): void {
|
|
811
|
+
if (this.#dismissTimer === undefined) return;
|
|
812
|
+
clearTimeout(this.#dismissTimer);
|
|
813
|
+
this.#dismissTimer = undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
#scheduleDismiss(generation: number, ctx: ExtensionContext): void {
|
|
817
|
+
if (this.successTtlMs <= 0) return;
|
|
818
|
+
this.#dismissTimer = setTimeout(() => {
|
|
819
|
+
this.#dismissTimer = undefined;
|
|
820
|
+
if (this.#current?.generation !== generation) return;
|
|
821
|
+
this.clear(ctx);
|
|
822
|
+
}, this.successTtlMs);
|
|
823
|
+
this.#dismissTimer.unref?.();
|
|
824
|
+
}
|
|
825
|
+
|
|
753
826
|
#isCurrent(requestId: string, generation: number): boolean {
|
|
754
827
|
return this.#current?.requestId === requestId &&
|
|
755
828
|
this.#current.generation === generation;
|