@estebanforge/pi-antigravity-bridge 1.8.6 → 1.8.7

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,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.8.7] - 2026-10-21
6
+
7
+ ### Changed
8
+
9
+ - **Consecutive `bridge_poll_result` polls collapse into one counted transcript line.** Each poll used to append its own display-only entry (observed live: 60 `Poll git_commit` entries from a single wait, flushed in bursts when the parked round-trip resolved). Poll events now fold into a single ♊︎-marked `agy-poll-run` entry per consecutive run (`♊︎ bridge_poll_result Poll git_commit (x30)`; expanded view shows the poll span) with a live dim status counter while a run is open. Runs close on a target switch, the next non-poll display event, turn end, and agent end. Bridge footer statuses (`agy-native`, `agy-usage`, the poll counter) now render muted, matching pi-hostname's dim treatment.
10
+
5
11
  ## [1.8.6] - 2026-10-20
6
12
 
7
13
  ### Changed
package/README.md CHANGED
@@ -42,7 +42,7 @@ The bridge starts a localhost MCP server inside pi's process. The exposed catalo
42
42
 
43
43
  **Upstream pi APIs only.** Bridge calls park in the provider's round-trip store; the provider ends the pi assistant message with a `toolUse` stop reason for the real pi tool, pi executes it in its own loop (native cards, permissions, hooks), and the toolResult completes the parked MCP response on the next stream call.
44
44
 
45
- **Long calls don't die.** agy's MCP client abandons a `tools/call` request at a flat ~180s, which used to kill any pi tool that ran longer (a long peer review, a build, a commit preview waiting for you). A call still running after ~20 seconds now settles its HTTP request with a `STILL RUNNING` answer carrying a `callId` while pi keeps executing; agy fetches the result through the bridge-local `bridge_poll_result` tool and polls until it lands. Delegation-shaped tools (pi subagent/delegation tools, e.g. `subagent_run`, `agent_spawn`) hold the synchronous window 120s instead (issue #6: orchestrators expect a plain result across the 30-120s subagent range, and 120s stays under the 180s wall); `escalateLongTools` in config.json adds custom names. Escalated calls get their own 30-minute budget, so human-gated tools can take as long as the human takes. Fast calls stay fully synchronous and never see any of this. If a park does fail (abort, timeout, recycle), the late result is re-routed to agy as a follow-up prompt in the same conversation instead of being lost.
45
+ **Long calls don't die.** agy's MCP client abandons a `tools/call` request at a flat ~180s, which used to kill any pi tool that ran longer (a long peer review, a build, a commit preview waiting for you). A call still running after ~20 seconds now settles its HTTP request with a `STILL RUNNING` answer carrying a `callId` while pi keeps executing; agy fetches the result through the bridge-local `bridge_poll_result` tool and polls until it lands. Delegation-shaped tools (pi subagent/delegation tools, e.g. `subagent_run`, `agent_spawn`) hold the synchronous window 120s instead (issue #6: orchestrators expect a plain result across the 30-120s subagent range, and 120s stays under the 180s wall); `escalateLongTools` in config.json adds custom names. Escalated calls get their own 30-minute budget, so human-gated tools can take as long as the human takes. Fast calls stay fully synchronous and never see any of this. If a park does fail (abort, timeout, recycle), the late result is re-routed to agy as a follow-up prompt in the same conversation instead of being lost. Consecutive polls render as ONE ♊︎-marked counted line (`♊︎ bridge_poll_result Poll git_commit (x30)`) instead of one entry per poll, with a live muted status counter while a run is open.
46
46
 
47
47
  **Recursion safety.** Only the provider's agy receives the extra `--add-dir`. `AskAntigravity` is also filtered from the exposed tool list. Its delegated `agy -p` spawns with just the workspace, and for the whole delegated run the bridge hides its global per-pid entries: disabled before the spawn, released only on process close or error - agy watches the config and hot-reloads MCP servers on file changes, so a mid-run re-enable would poke the live delegation back into pi. A cross-process marker (`suppression.json` in the bridge's extensions-data dir) coordinates concurrent delegations: entries come back only when no live delegation remains, a session starting mid-delegation registers its own entry disabled, and the session-start heal re-enables only once the last delegator is gone (dead entries pruned by pid liveness with a 24h age bound). The inner agy therefore sees no bridge and cannot re-enter pi. Foreign servers in the global config are never touched.
48
48
 
@@ -48,6 +48,7 @@ import {
48
48
  formatPollAnswer,
49
49
  type NativeDisplayEvent,
50
50
  } from "../src/provider.js";
51
+ import { PollRunFolder, type PollRunData } from "../src/poll-fold.js";
51
52
  import {
52
53
  createShadowTool,
53
54
  stripMarkerFields,
@@ -396,15 +397,52 @@ export default async function (pi: ExtensionAPI): Promise<void> {
396
397
  }
397
398
  return new Text(lines.join("\n"), 0, 0);
398
399
  });
400
+ // Collapsed poll runs: one ♊︎-marked counted line per consecutive poll
401
+ // burst instead of one entry per poll.
402
+ pi.registerEntryRenderer<PollRunData>("agy-poll-run", (entry, { expanded }, theme) => {
403
+ const run = entry.data;
404
+ const count = run?.count ?? 0;
405
+ const label = `Poll ${run?.target ?? "bridge call"} (x${count}${run?.state === "failed" ? ", failed" : ""})`;
406
+ const lines = [`♊︎ ${theme.fg("toolTitle", POLL_TOOL_NAME)} ${theme.fg("muted", label)}`];
407
+ if (expanded && run) {
408
+ const time = (iso: string): string => new Date(iso).toLocaleTimeString();
409
+ lines.push(theme.fg("dim", `${count} ${count === 1 ? "poll" : "polls"}, ${time(run.firstAt)} → ${time(run.lastAt)}`));
410
+ }
411
+ return new Text(lines.join("\n"), 0, 0);
412
+ });
399
413
  }
400
414
  let pendingNativeTools = 0;
415
+ const pollRuns = new PollRunFolder();
416
+ const closePollRun = (): void => {
417
+ const settled = pollRuns.flush();
418
+ if (settled) pi.appendEntry<PollRunData>("agy-poll-run", settled);
419
+ setDimStatus("agy-poll", undefined);
420
+ };
421
+ // Footer statuses render muted, matching pi-hostname's dim treatment.
422
+ const setDimStatus = (key: string, text: string | undefined): void => {
423
+ const ui = activeUi;
424
+ if (!ui) return;
425
+ ui.setStatus(key, text === undefined ? undefined : ui.theme.fg("dim", text));
426
+ };
401
427
  const onNativeEvent = (event: NativeDisplayEvent): void => {
402
- if (engine !== "acp" || event.mcpServer === "pi-bridge") return;
428
+ if (engine !== "acp") return;
429
+ // Polls are bridge-local (never get a genuine Pi card); the mcpServer
430
+ // tag is unreliable on these frames, so the tool name discriminates.
431
+ if (event.name === POLL_TOOL_NAME) {
432
+ const view = event.callId ? roundTrips.poll(event.callId) : undefined;
433
+ const switched = pollRuns.add(view?.name ?? "unknown call", view?.state ?? "running", new Date());
434
+ if (switched) pi.appendEntry<PollRunData>("agy-poll-run", switched);
435
+ const open = pollRuns.open;
436
+ setDimStatus("agy-poll", open ? `♊︎ bridge_poll_result → ${open.target} (x${open.count})` : undefined);
437
+ return;
438
+ }
439
+ closePollRun();
440
+ if (event.mcpServer === "pi-bridge") return;
403
441
  // MCP calls through our own pi-bridge already have genuine Pi tool cards;
404
442
  // only native Antigravity / other MCP events need display-only entries.
405
443
  if (event.status === "started") {
406
444
  pendingNativeTools++;
407
- activeUi?.setStatus("agy-native", `agy ${event.name}… (${pendingNativeTools})`);
445
+ setDimStatus("agy-native", `agy ${event.name}… (${pendingNativeTools})`);
408
446
  return;
409
447
  }
410
448
  pendingNativeTools = Math.max(0, pendingNativeTools - 1);
@@ -439,6 +477,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
439
477
  outcome.status === "OK" ? "info" : "warn",
440
478
  );
441
479
  roundTrips.failAll(FAIL_REASON_TURN_END);
480
+ // Closes a poll run that no display event flushed.
481
+ closePollRun();
442
482
  // Stamp live escalations with the ended turn's conversation: the park
443
483
  // pass returns before the provider's store.set, so a first-turn park
444
484
  // has no other record of where its result must be re-delivered.
@@ -454,7 +494,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
454
494
  pi.on("message_update", (event, ctx) => {
455
495
  if (!ctx.hasUI || event.message.role !== "assistant" || event.message.provider !== "antigravity" || loadConfig().acp.usageEstimate === "off") return;
456
496
  const { input, output } = event.message.usage;
457
- const status = `ACP ≈ ${input} in / ${output} out`;
497
+ const status = ctx.ui.theme.fg("dim", `ACP ≈ ${input} in / ${output} out`);
458
498
  if (status !== lastUsageStatus) {
459
499
  lastUsageStatus = status;
460
500
  ctx.ui.setStatus("agy-usage", status);
@@ -467,6 +507,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
467
507
  pi.on("agent_end", (event, ctx) => {
468
508
  clearUsageStatus(event, ctx);
469
509
  pendingNativeTools = 0;
510
+ closePollRun();
470
511
  if (ctx.hasUI) ctx.ui.setStatus("agy-native", undefined);
471
512
  });
472
513
  pi.on("session_shutdown", (event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.8.6",
3
+ "version": "1.8.7",
4
4
  "description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. Use antigravity/* models in Pi. Full /model picker support. Models runs Pi's tools. ToS safe to use.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -0,0 +1,55 @@
1
+ // Fold state machine for bridge_poll_result display events: consecutive
2
+ // polls collapse into ONE counted transcript entry instead of one entry per
3
+ // poll (observed live 2026-10-21: 60 "Poll git_commit" entries from a single
4
+ // wait, all flushed in bursts when the parked round-trip resolved).
5
+ //
6
+ // Runs close on a target switch, on any non-poll display event, or at turn
7
+ // end; the caller persists each closed run as an "agy-poll-run" entry.
8
+
9
+ export interface PollRunData {
10
+ /** The pi tool the model kept polling, e.g. "git_commit". */
11
+ target: string;
12
+ count: number;
13
+ /** Strongest poll outcome seen: running < done < failed. Sticky so a
14
+ * still-running poll after a terminal one cannot downgrade the summary. */
15
+ state: "running" | "done" | "failed";
16
+ firstAt: string;
17
+ lastAt: string;
18
+ }
19
+
20
+ const STATE_RANK: Record<PollRunData["state"], number> = { running: 0, done: 1, failed: 2 };
21
+
22
+ export class PollRunFolder {
23
+ #run: PollRunData | undefined;
24
+
25
+ /** The open run, for a live status-line counter. */
26
+ get open(): PollRunData | undefined {
27
+ return this.#run;
28
+ }
29
+
30
+ /** Fold one poll event. Returns a run to persist when this poll switched
31
+ * targets away from the open run; the new poll always starts or extends
32
+ * a run, so callers append the return value BEFORE reading `open`. */
33
+ add(target: string, state: PollRunData["state"], at: Date): PollRunData | undefined {
34
+ let closed: PollRunData | undefined;
35
+ if (this.#run && this.#run.target !== target) closed = this.#close();
36
+ if (!this.#run) {
37
+ this.#run = { target, count: 0, state: "running", firstAt: at.toISOString(), lastAt: at.toISOString() };
38
+ }
39
+ this.#run.count += 1;
40
+ this.#run.lastAt = at.toISOString();
41
+ if (STATE_RANK[state] > STATE_RANK[this.#run.state]) this.#run.state = state;
42
+ return closed;
43
+ }
44
+
45
+ /** Close and return the open run, if any. Idempotent. */
46
+ flush(): PollRunData | undefined {
47
+ return this.#run ? this.#close() : undefined;
48
+ }
49
+
50
+ #close(): PollRunData {
51
+ const run = this.#run as PollRunData;
52
+ this.#run = undefined;
53
+ return run;
54
+ }
55
+ }
package/src/provider.ts CHANGED
@@ -332,6 +332,9 @@ export interface NativeDisplayEvent {
332
332
  name: string;
333
333
  mcpServer?: string;
334
334
  status: "started" | "completed" | "failed";
335
+ /** Bridge callId from the tool args (poll calls carry one); lets the
336
+ * extension resolve the polled pi tool via the escalation registry. */
337
+ callId?: string;
335
338
  path?: string;
336
339
  command?: string;
337
340
  output?: string;
@@ -1143,10 +1146,16 @@ export function consumeActivity(
1143
1146
  // model-facing result is produced. Persist a card only on completion.
1144
1147
  if (feats.engine === "acp" && feats.onNativeEvent) {
1145
1148
  try {
1146
- feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "started" });
1149
+ feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "started", callId: callIdArg(activity.args) });
1147
1150
  } catch { /* UI failure must not fail the generation. */ }
1148
1151
  }
1149
1152
  return "continue";
1153
+ /** Bridge callId arg of an MCP tool call (bridge_poll_result requires it). */
1154
+ function callIdArg(args: Record<string, unknown>): string | undefined {
1155
+ const v = args.callId;
1156
+ return typeof v === "string" && v.length > 0 ? v : undefined;
1157
+ }
1158
+
1150
1159
  /** File-path argument of an ACP edit tool (observed: `file_path`); other
1151
1160
  * tool kinds (execute, read) don't carry one. Returns undefined for
1152
1161
  * non-edit tools. */
@@ -1183,7 +1192,7 @@ function acpEditFileArg(args: Record<string, unknown>): string | undefined {
1183
1192
  let displayed = false;
1184
1193
  try {
1185
1194
  if (feats.onNativeEvent) {
1186
- feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "completed", path: file, command, output: activity.output, diff: diffText });
1195
+ feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "completed", path: file, command, output: activity.output, diff: diffText, callId: callIdArg(activity.args) });
1187
1196
  displayed = true;
1188
1197
  }
1189
1198
  } catch { /* A stale renderer must never fail the model turn. */ }