@estebanforge/pi-antigravity-bridge 1.5.2 → 1.5.3

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
@@ -2,7 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [1.5.2] - 2026-09-15
5
+ ## [1.5.3] - 2026-09-10
6
+
7
+ ### Fixed
8
+
9
+ - **Parallel agy tool approvals no longer clobber each other.** The shadow-tool `GatePolicy` asked through pi's `ui.confirm`, but pi's TUI shows ONE extension dialog at a time and an overlapping call replaces the live dialog without settling it: with several agy bash/write/edit calls in flight, approvals were silently lost (the park timeout eventually resolved them as declines). The confirm now holds the same shared cross-extension dialog lock as the pi-*-me gates (`withDialogLock`, `Symbol.for("pi-me.dialog-lock")`), so approvals queue and render in turn; the park timeout still bounds each dialog and is held inside the lock. New `src/dialog-lock.ts` module plus contract tests (FIFO order, throw-release, shared key).
10
+
11
+ ## [1.5.2] - 2026-09-09
6
12
 
7
13
  ### Fixed
8
14
 
@@ -88,6 +88,7 @@ import {
88
88
  import { mapAgyToolToNative } from "../src/native-tools.js";
89
89
  import { Type } from "typebox";
90
90
  import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
91
+ import { withDialogLock } from "../src/dialog-lock.js";
91
92
 
92
93
  // Last UI seen (session_start / /agy commands). The ACP login URL arrives
93
94
  // via the driver log sink, which has no command context; the stash lets that
@@ -808,7 +809,14 @@ export default async function (pi: ExtensionAPI): Promise<void> {
808
809
  : typeof params.path === "string"
809
810
  ? params.path
810
811
  : JSON.stringify(stripMarkerFields(params)).slice(0, 200);
811
- const ok = await extCtx.ui.confirm(`agy ${tool}?`, what, { timeout: APPROVAL_PARK_MS });
812
+ // Dialog lock: parallel agy tool approvals queue up instead of
813
+ // clobbering the live dialog (which silently loses the approval).
814
+ // Capture the narrowed method: TS drops the guard's narrowing
815
+ // inside the deferred closure.
816
+ const uiConfirm = extCtx.ui.confirm.bind(extCtx.ui);
817
+ const ok = await withDialogLock(() =>
818
+ uiConfirm(`agy ${tool}?`, what, { timeout: APPROVAL_PARK_MS }),
819
+ );
812
820
  return ok ? { allow: true } : { allow: false, reason: `declined in pi (agy ${tool})` };
813
821
  };
814
822
  const handle = r.handle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -0,0 +1,48 @@
1
+ // Process-wide FIFO lock for human-facing dialogs (confirm/editor/select).
2
+ //
3
+ // pi's interactive UI shows ONE extension dialog at a time; an overlapping
4
+ // ctx.ui.* dialog call REPLACES the live dialog and the replaced promise
5
+ // never settles, so parallel gated tool calls hang forever (or silently lose
6
+ // their prompt). Holding this lock around each dialog serializes them: the
7
+ // first renders, the rest appear in turn as each is answered.
8
+ //
9
+ // The lock is keyed via Symbol.for because ALL pi-* extensions run in the
10
+ // SAME pi process and must share one queue: a per-module lock would still let
11
+ // a dialog from one extension clobber a dialog from another in the same
12
+ // parallel tool-call batch.
13
+ const DIALOG_LOCK: unique symbol = Symbol.for("pi-me.dialog-lock");
14
+
15
+ interface DialogQueue {
16
+ tail: Promise<void>;
17
+ }
18
+
19
+ function dialogQueue(): DialogQueue {
20
+ const host = globalThis as typeof globalThis & Record<symbol, unknown>;
21
+ const existing = host[DIALOG_LOCK] as DialogQueue | undefined;
22
+ if (existing) return existing;
23
+ const created: DialogQueue = { tail: Promise.resolve() };
24
+ host[DIALOG_LOCK] = created;
25
+ return created;
26
+ }
27
+
28
+ /**
29
+ * Run `run` while holding the cross-extension dialog lock. FIFO: each caller
30
+ * chains onto the queue tail synchronously (before its first await), so call
31
+ * order is the order the dialogs appear. Released in a finally block, so one
32
+ * throwing dialog can never wedge the queue for the callers behind it.
33
+ * NOT reentrant: calling it inside a held `run` self-deadlocks.
34
+ */
35
+ export async function withDialogLock<T>(run: () => Promise<T>): Promise<T> {
36
+ const queue = dialogQueue();
37
+ const prev = queue.tail;
38
+ let release!: () => void;
39
+ queue.tail = new Promise<void>((resolve) => {
40
+ release = resolve;
41
+ });
42
+ await prev;
43
+ try {
44
+ return await run();
45
+ } finally {
46
+ release();
47
+ }
48
+ }