@estebanforge/pi-ask-antigravity 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.
Files changed (2) hide show
  1. package/extensions/index.ts +191 -2
  2. package/package.json +1 -1
@@ -34,11 +34,15 @@ import * as os from "node:os";
34
34
  import * as path from "node:path";
35
35
  import {
36
36
  type AgentToolResult,
37
+ buildSessionContext,
38
+ getAgentDir,
37
39
  getSettingsListTheme,
40
+ keyHint,
38
41
  type ExtensionAPI,
39
42
  } from "@earendil-works/pi-coding-agent";
40
43
 
41
44
  import { Container, SettingsList, Text, type SettingItem } from "@earendil-works/pi-tui";
45
+ import { contentText } from "@earendil-works/pi-ai";
42
46
  import { Type } from "typebox";
43
47
 
44
48
  // --- Constants -------------------------------------------------------------
@@ -52,6 +56,10 @@ const DISCOVERY_POLL_ATTEMPTS = 5;
52
56
  const DISCOVERY_POLL_MS = 100;
53
57
  const GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "ask-antigravity.json");
54
58
 
59
+ // renderCall / renderResult preview limits (match pi-claude-bridge).
60
+ const PREVIEW_MAX_CHARS = 1000;
61
+ const PREVIEW_MAX_LINES = 6;
62
+
55
63
  const DEFAULT_MODEL = "flash";
56
64
  const DEFAULT_THINKING = "medium";
57
65
  // Default on: without --dangerously-skip-permissions, any run_command hangs in
@@ -645,14 +653,82 @@ function newConversationId(
645
653
  return null;
646
654
  }
647
655
 
656
+ // --- Full-context export (opt-in includeContext) --------------------------
657
+ // NOTE: duplicated per pi-ask-* package (each is self-contained). Duck-typed
658
+ // over role/content to tolerate AgentMessage's union + custom message types.
659
+
660
+ // Tool-call inputs and tool-result bodies are clamped so the exported
661
+ // transcript stays reviewable; the agent can re-read any source file by path.
662
+ // User/assistant prose is kept in full (that IS the conversation).
663
+ const CONTEXT_BLOCK_MAX_CHARS = 2000;
664
+
665
+ function clampBlock(text: unknown, limit = CONTEXT_BLOCK_MAX_CHARS): string {
666
+ const t = String(text ?? "");
667
+ return t.length > limit ? `${t.slice(0, limit)}\n…[truncated, ${t.length - limit} more chars]` : t;
668
+ }
669
+
670
+ /** Render resolved pi AgentMessages to a readable markdown transcript.
671
+ * Pure: no IO. Caller writes the returned string to a temp file. */
672
+ export function renderAgentMessagesMarkdown(messages: readonly unknown[]): string {
673
+ const lines: string[] = [
674
+ "# Pi conversation context",
675
+ "",
676
+ `_Exported for full-context delegation. ${messages.length} message(s)._`,
677
+ "",
678
+ ];
679
+ for (const raw of messages) {
680
+ const m = raw as { role?: string; content?: unknown };
681
+ const role = m.role ?? "message";
682
+ const content = m.content;
683
+ if (role === "assistant") {
684
+ const blocks = (Array.isArray(content) ? content : []) as ReadonlyArray<{
685
+ type: string;
686
+ text?: string;
687
+ name?: string;
688
+ input?: unknown;
689
+ }>;
690
+ const text = blocks
691
+ .filter((b) => b.type === "text")
692
+ .map((b) => b.text ?? "")
693
+ .join("\n");
694
+ if (text.trim()) lines.push("## Assistant", "", text, "");
695
+ for (const b of blocks) {
696
+ if (b.type === "toolCall" || b.type === "tool_use") {
697
+ const input = clampBlock(
698
+ typeof b.input === "string" ? b.input : JSON.stringify(b.input ?? ""),
699
+ 500,
700
+ );
701
+ lines.push(`> tool call: ${b.name ?? "(unknown)"}(${input})`, "");
702
+ }
703
+ }
704
+ } else if (role === "toolResult" || role === "tool_result" || role === "tool") {
705
+ const text = contentText(content as any);
706
+ if (text.trim()) lines.push("## Tool result", "", clampBlock(text), "");
707
+ } else {
708
+ const text = contentText(content as any);
709
+ if (text.trim()) lines.push(`## ${role}`, "", clampBlock(text), "");
710
+ }
711
+ }
712
+ return lines.join("\n");
713
+ }
714
+
715
+ /** Centralized scratch dir for full-context exports, following the
716
+ * ~/.pi/extensions-data/<author>/<extension>/ convention (see pi-token-cost-ledger).
717
+ * Derived from getAgentDir() so rebranded distros resolve correctly. */
718
+ function askContextDir(): string {
719
+ return path.join(path.dirname(getAgentDir()), "extensions-data", "estebanforge", "pi-ask-antigravity");
720
+ }
721
+
648
722
  // --- Extension -------------------------------------------------------------
649
723
 
650
724
  interface AgyDetails {
651
725
  model: string | null;
652
726
  resolvedModel: string | null;
727
+ thinking: ThinkingTier | null;
653
728
  mode: Mode;
654
729
  digest: boolean;
655
730
  conversationId: string | null;
731
+ includeContext: boolean;
656
732
  exitCode: number;
657
733
  aborted: boolean;
658
734
  timedOut: boolean;
@@ -855,7 +931,80 @@ export default async function (pi: ExtensionAPI) {
855
931
  description: `Hard cap on the agy run in minutes. Default ${DEFAULT_TIMEOUT_MIN}.`,
856
932
  }),
857
933
  ),
934
+ includeContext: Type.Optional(
935
+ Type.Boolean({
936
+ description:
937
+ "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.",
938
+ }),
939
+ ),
858
940
  }),
941
+ renderCall(args, theme, _context) {
942
+ // Show RESOLVED model/thinking/mode (config defaults applied) so the
943
+ // row identifies what will actually run, not just explicit args.
944
+ // agy folds the thinking tier into the model alias or --effort; we
945
+ // surface the resolved tier separately for identification.
946
+ const cfg = loadConfig();
947
+ const requestedModel = (args.model as string | undefined)?.trim() || cfg.defaultModel;
948
+ const resolved =
949
+ resolveModel(requestedModel, discovered, cfg.defaultThinking) ?? { model: requestedModel };
950
+ const thinking: ThinkingTier = resolved.effort ?? cfg.defaultThinking;
951
+ const mode: Mode = (args.mode as Mode | undefined) ?? "accept-edits";
952
+ const useDigest = typeof args.digest === "boolean" ? args.digest : mode === "plan";
953
+ const isContinue =
954
+ typeof args.conversationId === "string" && CONV_ID_RE.test(args.conversationId);
955
+
956
+ const tags: string[] = [`model=${resolved.model}`, `thinking=${thinking}`];
957
+ if (mode !== "accept-edits") tags.push(`mode=${mode}`);
958
+ if (useDigest) tags.push("digest");
959
+ if (isContinue) tags.push("continue");
960
+ if (args.includeContext) tags.push("context=full");
961
+
962
+ let text = theme.fg("mdLink", theme.bold("AskAntigravity "));
963
+ text += `${theme.fg("accent", `[${tags.join(", ")}]`)} `;
964
+
965
+ const prompt = String(args.prompt ?? "");
966
+ const truncated = prompt.length > PREVIEW_MAX_CHARS ? prompt.slice(0, PREVIEW_MAX_CHARS) : prompt;
967
+ const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
968
+ text += theme.fg("muted", `"${lines.join("\n")}"`);
969
+ if (prompt.length > PREVIEW_MAX_CHARS || prompt.split("\n").length > PREVIEW_MAX_LINES) {
970
+ text += theme.fg("dim", " …");
971
+ }
972
+ return new Text(text, 0, 0);
973
+ },
974
+ renderResult(result, { expanded, isPartial }, theme) {
975
+ const d = result.details as AgyDetails | undefined;
976
+ if (isPartial) {
977
+ const status = result.content[0]?.type === "text" ? result.content[0].text : "working...";
978
+ return new Text(theme.fg("mdLink", "◉ AskAntigravity ") + theme.fg("muted", status), 0, 0);
979
+ }
980
+
981
+ const body = result.content[0]?.type === "text" ? result.content[0].text : "";
982
+ const errored = d?.exitCode !== 0 || !!d?.aborted || !!d?.timedOut;
983
+
984
+ let text = errored
985
+ ? theme.fg("error", "✗ AskAntigravity error")
986
+ : theme.fg("mdLink", "✓ AskAntigravity");
987
+
988
+ const rTags: string[] = [];
989
+ if (d?.resolvedModel || d?.model) rTags.push(`model=${d?.resolvedModel ?? d?.model}`);
990
+ if (d?.thinking) rTags.push(`thinking=${d.thinking}`);
991
+ if (d?.mode && d.mode !== "accept-edits") rTags.push(`mode=${d.mode}`);
992
+ if (d?.includeContext) rTags.push("context=full");
993
+ if (rTags.length) text += ` ${theme.fg("accent", `[${rTags.join(", ")}]`)}`;
994
+ if (d?.durationMs) text += ` ${theme.fg("dim", `${(d.durationMs / 1000).toFixed(1)}s`)}`;
995
+
996
+ if (expanded) {
997
+ if (body) text += `\n${theme.fg("toolOutput", body)}`;
998
+ } else {
999
+ const truncated = body.length > PREVIEW_MAX_CHARS ? body.slice(0, PREVIEW_MAX_CHARS) : body;
1000
+ const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
1001
+ if (lines.length) text += `\n${theme.fg("toolOutput", lines.join("\n"))}`;
1002
+ if (body.length > PREVIEW_MAX_CHARS || body.split("\n").length > PREVIEW_MAX_LINES) {
1003
+ text += `\n${theme.fg("dim", `… (${keyHint("app.tools.expand", "to expand")})`)}`;
1004
+ }
1005
+ }
1006
+ return new Text(text, 0, 0);
1007
+ },
859
1008
  async execute(toolCallId, params, signal, onUpdate, ctx) {
860
1009
  // Circular-delegation guard (best-effort). This extension registers
861
1010
  // NO provider, so the check only fires if a future agy-as-provider
@@ -951,6 +1100,29 @@ export default async function (pi: ExtensionAPI) {
951
1100
  ? `(Use compact digests, not full file contents.)\n${params.prompt}`
952
1101
  : params.prompt;
953
1102
 
1103
+ // Opt-in full-context export (isolated stays the default).
1104
+ let contextFile: string | null = null;
1105
+ if (params.includeContext) {
1106
+ try {
1107
+ const { messages } = buildSessionContext(ctx.sessionManager.getBranch());
1108
+ if (messages.length) {
1109
+ const md = renderAgentMessagesMarkdown(messages);
1110
+ const ctxDir = askContextDir();
1111
+ fs.mkdirSync(ctxDir, { recursive: true });
1112
+ contextFile = path.join(
1113
+ ctxDir,
1114
+ `.ask-context-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.md`,
1115
+ );
1116
+ fs.writeFileSync(contextFile, md, { mode: 0o600 });
1117
+ }
1118
+ } catch {
1119
+ contextFile = null;
1120
+ }
1121
+ }
1122
+ const effectivePrompt = contextFile
1123
+ ? `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}`
1124
+ : finalPrompt;
1125
+
954
1126
  const args: string[] = ["--add-dir", cwd];
955
1127
  const extra = extraArgs();
956
1128
  if (extra.length) args.push(...extra);
@@ -963,14 +1135,17 @@ export default async function (pi: ExtensionAPI) {
963
1135
  if (config.skipPermissions) args.push("--dangerously-skip-permissions");
964
1136
  if (isContinuation) args.push("--conversation", rawConvId as string);
965
1137
  args.push("--print-timeout", `${timeoutMin}m`);
966
- args.push("-p", finalPrompt);
1138
+ if (contextFile) args.push("--add-dir", askContextDir());
1139
+ args.push("-p", effectivePrompt);
967
1140
 
968
1141
  const details: AgyDetails = {
969
1142
  model: requestedModel,
970
1143
  resolvedModel: resolved.model,
1144
+ thinking: resolved.effort ?? config.defaultThinking,
971
1145
  mode,
972
1146
  digest: useDigest,
973
1147
  conversationId: isContinuation ? (rawConvId as string) : null,
1148
+ includeContext: contextFile !== null,
974
1149
  exitCode: 0,
975
1150
  aborted: false,
976
1151
  timedOut: false,
@@ -1211,16 +1386,30 @@ export default async function (pi: ExtensionAPI) {
1211
1386
  details,
1212
1387
  };
1213
1388
  }
1389
+ finally {
1390
+ if (contextFile) {
1391
+ try {
1392
+ fs.unlinkSync(contextFile);
1393
+ } catch {}
1394
+ }
1395
+ }
1214
1396
  },
1215
1397
  });
1216
1398
  }
1217
1399
 
1218
- function emptyDetails(model: string | null, resolvedModel: string | null): AgyDetails {
1400
+ function emptyDetails(
1401
+ model: string | null,
1402
+ resolvedModel: string | null,
1403
+ thinking: ThinkingTier | null = null,
1404
+ includeContext: boolean = false,
1405
+ ): AgyDetails {
1219
1406
  return {
1220
1407
  model,
1221
1408
  resolvedModel,
1409
+ thinking,
1222
1410
  mode: "accept-edits",
1223
1411
  digest: false,
1412
+ includeContext,
1224
1413
  conversationId: null,
1225
1414
  exitCode: 0,
1226
1415
  aborted: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-ask-antigravity",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Pi tool that delegates a self-contained sub-task to Google Antigravity's agy CLI (the CLI for Gemini). Friendly model aliases (flash/pro/gemini), configurable default model + thinking, and the AskAntigravity delegation tool.",
5
5
  "keywords": [
6
6
  "pi-package",