@estebanforge/pi-antigravity-bridge 1.2.3 → 1.2.4

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,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.2.4] - 2026-08-13
6
+
7
+ ### Added
8
+
9
+ - **Model, thinking tier, and mode shown next to the tool name.** The
10
+ `AskAntigravity` tool now renders
11
+ `AskAntigravity [model=gemini-3.6-flash, thinking=high]` with a prompt
12
+ preview while a delegation runs, plus a tidy result row
13
+ (`✓ AskAntigravity 12.3s`) with an expandable body. Built on pi's
14
+ `renderCall`/`renderResult` hooks; the values shown are the resolved
15
+ config defaults (model alias + tier), not just the args the caller passed.
16
+ `AgyDetails` gained a `thinking` field.
17
+ - **Opt-in full-context delegation (`includeContext`).** New boolean param
18
+ (default `false`, isolated one-shot unchanged). When `true`, the current pi
19
+ conversation is exported as resolved markdown to
20
+ `~/.pi/extensions-data/estebanforge/pi-antigravity-bridge/` and the prompt
21
+ tells agy to read it first. The run passes `--add-dir` for that folder so
22
+ the sandbox can read it; the temp file is removed after the run.
23
+
5
24
  ## [1.2.3] - 2026-08-10
6
25
 
7
26
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker via SQLite polling + protobuf decode of agy's conversation DBs.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/ask-tool.ts CHANGED
@@ -15,6 +15,9 @@ import { spawn } from "node:child_process";
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+ import { buildSessionContext, getAgentDir, keyHint } from "@earendil-works/pi-coding-agent";
19
+ import { Text } from "@earendil-works/pi-tui";
20
+ import { contentText } from "@earendil-works/pi-ai";
18
21
  import { Type } from "typebox";
19
22
  import {
20
23
  CONVERSATIONS_DIR,
@@ -30,6 +33,10 @@ const DEFAULT_TIMEOUT_MIN = 10;
30
33
  const GRACE_AFTER_TIMEOUT_MS = 5000;
31
34
  const STATUS_INTERVAL_MS = 1000;
32
35
  const STATUS_TAIL_CHARS = 160;
36
+
37
+ // renderCall / renderResult preview limits (match pi-claude-bridge).
38
+ const PREVIEW_MAX_CHARS = 1000;
39
+ const PREVIEW_MAX_LINES = 6;
33
40
  const DISCOVERY_POLL_ATTEMPTS = 5;
34
41
  const DISCOVERY_POLL_MS = 100;
35
42
 
@@ -302,7 +309,78 @@ export async function registerAskAntigravityTool(
302
309
  timeoutMinutes: Type.Optional(
303
310
  Type.Number({ description: `Hard cap on the agy run in minutes. Default ${DEFAULT_TIMEOUT_MIN}.` }),
304
311
  ),
312
+ includeContext: Type.Optional(
313
+ Type.Boolean({
314
+ description:
315
+ "When true, export the current pi conversation (resolved, as markdown) to a temp file inside the workspace and tell agy to read it first. Default false (isolated one-shot). Opt in only when the user explicitly wants agy to see the full conversation; it costs agy tokens to read.",
316
+ }),
317
+ ),
305
318
  }),
319
+ renderCall(args, theme, _context) {
320
+ // Show RESOLVED model/thinking/mode (config defaults applied) so the
321
+ // row identifies what will actually run, not just explicit args.
322
+ const cfg = loadConfig();
323
+ const requestedModel = (args.model as string | undefined)?.trim() || cfg.defaultModel;
324
+ const resolved =
325
+ resolveModel(requestedModel, entries, cfg.defaultThinking) ?? { model: requestedModel };
326
+ const thinking: ThinkingTier = resolved.effort ?? cfg.defaultThinking;
327
+ const mode: AgyMode = (args.mode as AgyMode | undefined) ?? "accept-edits";
328
+ const useDigest = typeof args.digest === "boolean" ? args.digest : mode === "plan";
329
+ const isContinue =
330
+ typeof args.conversationId === "string" && CONV_ID_RE.test(args.conversationId);
331
+
332
+ const tags: string[] = [`model=${resolved.model}`, `thinking=${thinking}`];
333
+ if (mode !== "accept-edits") tags.push(`mode=${mode}`);
334
+ if (useDigest) tags.push("digest");
335
+ if (isContinue) tags.push("continue");
336
+ if (args.includeContext) tags.push("context=full");
337
+
338
+ let text = theme.fg("mdLink", theme.bold("AskAntigravity "));
339
+ text += `${theme.fg("accent", `[${tags.join(", ")}]`)} `;
340
+
341
+ const prompt = String(args.prompt ?? "");
342
+ const truncated = prompt.length > PREVIEW_MAX_CHARS ? prompt.slice(0, PREVIEW_MAX_CHARS) : prompt;
343
+ const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
344
+ text += theme.fg("muted", `"${lines.join("\n")}"`);
345
+ if (prompt.length > PREVIEW_MAX_CHARS || prompt.split("\n").length > PREVIEW_MAX_LINES) {
346
+ text += theme.fg("dim", " …");
347
+ }
348
+ return new Text(text, 0, 0);
349
+ },
350
+ renderResult(result, { expanded, isPartial }, theme) {
351
+ const d = result.details as AgyDetails | undefined;
352
+ if (isPartial) {
353
+ const status = result.content[0]?.type === "text" ? result.content[0].text : "working...";
354
+ return new Text(theme.fg("mdLink", "◉ AskAntigravity ") + theme.fg("muted", status), 0, 0);
355
+ }
356
+
357
+ const body = result.content[0]?.type === "text" ? result.content[0].text : "";
358
+ const errored = d?.exitCode !== 0 || !!d?.aborted || !!d?.timedOut;
359
+
360
+ let text = errored
361
+ ? theme.fg("error", "✗ AskAntigravity error")
362
+ : theme.fg("mdLink", "✓ AskAntigravity");
363
+
364
+ const rTags: string[] = [];
365
+ if (d?.resolvedModel || d?.model) rTags.push(`model=${d?.resolvedModel ?? d?.model}`);
366
+ if (d?.thinking) rTags.push(`thinking=${d.thinking}`);
367
+ if (d?.mode && d.mode !== "accept-edits") rTags.push(`mode=${d.mode}`);
368
+ if (d?.includeContext) rTags.push("context=full");
369
+ if (rTags.length) text += ` ${theme.fg("accent", `[${rTags.join(", ")}]`)}`;
370
+ if (d?.durationMs) text += ` ${theme.fg("dim", `${(d.durationMs / 1000).toFixed(1)}s`)}`;
371
+
372
+ if (expanded) {
373
+ if (body) text += `\n${theme.fg("toolOutput", body)}`;
374
+ } else {
375
+ const truncated = body.length > PREVIEW_MAX_CHARS ? body.slice(0, PREVIEW_MAX_CHARS) : body;
376
+ const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
377
+ if (lines.length) text += `\n${theme.fg("toolOutput", lines.join("\n"))}`;
378
+ if (body.length > PREVIEW_MAX_CHARS || body.split("\n").length > PREVIEW_MAX_LINES) {
379
+ text += `\n${theme.fg("dim", `… (${keyHint("app.tools.expand", "to expand")})`)}`;
380
+ }
381
+ }
382
+ return new Text(text, 0, 0);
383
+ },
306
384
  async execute(toolCallId, params, signal, onUpdate, ctx) {
307
385
  // Circular-delegation guard: refuse if already running through the
308
386
  // antigravity provider.
@@ -367,6 +445,29 @@ export async function registerAskAntigravityTool(
367
445
  ? `(Use compact digests, not full file contents.)\n${params.prompt}`
368
446
  : params.prompt;
369
447
 
448
+ // Opt-in full-context export (isolated stays the default).
449
+ let contextFile: string | null = null;
450
+ if (params.includeContext) {
451
+ try {
452
+ const { messages } = buildSessionContext(ctx.sessionManager.getBranch());
453
+ if (messages.length) {
454
+ const md = renderAgentMessagesMarkdown(messages);
455
+ const ctxDir = askContextDir();
456
+ fs.mkdirSync(ctxDir, { recursive: true });
457
+ contextFile = path.join(
458
+ ctxDir,
459
+ `.ask-context-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.md`,
460
+ );
461
+ fs.writeFileSync(contextFile, md, { mode: 0o600 });
462
+ }
463
+ } catch {
464
+ contextFile = null;
465
+ }
466
+ }
467
+ const effectivePrompt = contextFile
468
+ ? `The full pi conversation context (as markdown) is at: ${contextFile}\nRead that file first for context, then do the task below.\n\n---\n\n${finalPrompt}`
469
+ : finalPrompt;
470
+
370
471
  const args: string[] = ["--add-dir", cwd];
371
472
  const extra = extraArgs();
372
473
  if (extra.length) args.push(...extra);
@@ -379,14 +480,17 @@ export async function registerAskAntigravityTool(
379
480
  if (config.skipPermissions !== false) args.push("--dangerously-skip-permissions");
380
481
  if (isContinuation) args.push("--conversation", rawConvId as string);
381
482
  args.push("--print-timeout", `${timeoutMin}m`);
382
- args.push("-p", finalPrompt);
483
+ if (contextFile) args.push("--add-dir", askContextDir());
484
+ args.push("-p", effectivePrompt);
383
485
 
384
486
  const details: AgyDetails = {
385
487
  model: requestedModel,
386
488
  resolvedModel: resolved.model,
489
+ thinking: resolved.effort ?? config.defaultThinking,
387
490
  mode,
388
491
  digest: useDigest,
389
492
  conversationId: isContinuation ? (rawConvId as string) : null,
493
+ includeContext: contextFile !== null,
390
494
  exitCode: 0,
391
495
  aborted: false,
392
496
  timedOut: false,
@@ -578,16 +682,91 @@ export async function registerAskAntigravityTool(
578
682
  const msg = err instanceof Error ? err.message : String(err);
579
683
  return { content: [{ type: "text", text: `failed to run agy: ${msg}` }], details };
580
684
  }
685
+ finally {
686
+ if (contextFile) {
687
+ try {
688
+ fs.unlinkSync(contextFile);
689
+ } catch {}
690
+ }
691
+ }
581
692
  },
582
693
  });
583
694
  }
584
695
 
696
+ // --- Full-context export (opt-in includeContext) --------------------------
697
+ // NOTE: duplicated per pi-ask-* / bridge package (each is self-contained).
698
+ // Duck-typed over role/content to tolerate AgentMessage's union + custom types.
699
+
700
+ // Tool-call inputs and tool-result bodies are clamped so the exported
701
+ // transcript stays reviewable; the agent can re-read any source file by path.
702
+ // User/assistant prose is kept in full (that IS the conversation).
703
+ const CONTEXT_BLOCK_MAX_CHARS = 2000;
704
+
705
+ function clampBlock(text: unknown, limit = CONTEXT_BLOCK_MAX_CHARS): string {
706
+ const t = String(text ?? "");
707
+ return t.length > limit ? `${t.slice(0, limit)}\n…[truncated, ${t.length - limit} more chars]` : t;
708
+ }
709
+
710
+ /** Render resolved pi AgentMessages to a readable markdown transcript.
711
+ * Pure: no IO. Caller writes the returned string to a temp file. */
712
+ function renderAgentMessagesMarkdown(messages: readonly unknown[]): string {
713
+ const lines: string[] = [
714
+ "# Pi conversation context",
715
+ "",
716
+ `_Exported for full-context delegation. ${messages.length} message(s)._`,
717
+ "",
718
+ ];
719
+ for (const raw of messages) {
720
+ const m = raw as { role?: string; content?: unknown };
721
+ const role = m.role ?? "message";
722
+ const content = m.content;
723
+ if (role === "assistant") {
724
+ const blocks = (Array.isArray(content) ? content : []) as ReadonlyArray<{
725
+ type: string;
726
+ text?: string;
727
+ name?: string;
728
+ input?: unknown;
729
+ }>;
730
+ const text = blocks
731
+ .filter((b) => b.type === "text")
732
+ .map((b) => b.text ?? "")
733
+ .join("\n");
734
+ if (text.trim()) lines.push("## Assistant", "", text, "");
735
+ for (const b of blocks) {
736
+ if (b.type === "toolCall" || b.type === "tool_use") {
737
+ const input = clampBlock(
738
+ typeof b.input === "string" ? b.input : JSON.stringify(b.input ?? ""),
739
+ 500,
740
+ );
741
+ lines.push(`> tool call: ${b.name ?? "(unknown)"}(${input})`, "");
742
+ }
743
+ }
744
+ } else if (role === "toolResult" || role === "tool_result" || role === "tool") {
745
+ const text = contentText(content as any);
746
+ if (text.trim()) lines.push("## Tool result", "", clampBlock(text), "");
747
+ } else {
748
+ const text = contentText(content as any);
749
+ if (text.trim()) lines.push(`## ${role}`, "", clampBlock(text), "");
750
+ }
751
+ }
752
+ return lines.join("\n");
753
+ }
754
+
755
+ /** Centralized scratch dir for full-context exports, following the
756
+ * ~/.pi/extensions-data/<author>/<extension>/ convention (see pi-token-cost-ledger).
757
+ * Derived from getAgentDir() so rebranded distros resolve correctly. */
758
+ function askContextDir(): string {
759
+ return path.join(path.dirname(getAgentDir()), "extensions-data", "estebanforge", "pi-antigravity-bridge");
760
+ }
761
+
585
762
  interface AgyDetails {
586
763
  model: string | null;
587
764
  resolvedModel: string | null;
765
+ thinking: ThinkingTier | null;
588
766
  mode: AgyMode;
589
767
  digest: boolean;
590
768
  conversationId: string | null;
769
+ includeContext: boolean;
591
770
  exitCode: number;
592
771
  aborted: boolean;
593
772
  timedOut: boolean;
@@ -595,12 +774,19 @@ interface AgyDetails {
595
774
  stderr: string;
596
775
  }
597
776
 
598
- function emptyDetails(model: string | null = null, resolvedModel: string | null = null): AgyDetails {
777
+ function emptyDetails(
778
+ model: string | null = null,
779
+ resolvedModel: string | null = null,
780
+ thinking: ThinkingTier | null = null,
781
+ includeContext: boolean = false,
782
+ ): AgyDetails {
599
783
  return {
600
784
  model,
601
785
  resolvedModel,
786
+ thinking,
602
787
  mode: "accept-edits",
603
788
  digest: false,
789
+ includeContext,
604
790
  conversationId: null,
605
791
  exitCode: 0,
606
792
  aborted: false,