@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 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 29.3.0 through the
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; the latest result
182
- remains visible until then and is cleared when the session changes or shuts
183
- down. Concurrent older checks cannot overwrite the most recently started one.
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.17.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": ">=29.3.0 <32.0.0"
52
+ "@gotgenes/pi-permission-system": ">=30.0.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@earendil-works/pi-ai": "^0.85.0",
@@ -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
- ? "reviewing"
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
- `${verbAnsi}${verb}\x1b[0m${mutedAnsi}`,
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
- ? (_tui, theme) => ({
642
- render: (width: number) =>
643
- renderUserReviewWidgetLines(data, theme, width),
644
- invalidate() {},
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;