@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 +19 -0
- package/package.json +1 -1
- package/src/ask-tool.ts +188 -2
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
|
+
"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("-
|
|
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(
|
|
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,
|