@estebanforge/pi-ask-antigravity 1.2.1 → 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/extensions/index.ts +254 -37
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
@@ -83,13 +91,13 @@ type Mode = "plan" | "accept-edits";
|
|
|
83
91
|
// whose full string equals the overlay target, the live entry wins. When it
|
|
84
92
|
// does not (older agy, missing model, plan gate), the overlay entry resolves
|
|
85
93
|
// the alias so the user can still type "sonnet" and get a working answer.
|
|
86
|
-
// "sonnet" ->
|
|
87
|
-
// "opus" ->
|
|
88
|
-
// "gpt-oss" ->
|
|
94
|
+
// "sonnet" -> claude-sonnet-4-6
|
|
95
|
+
// "opus" -> claude-opus-4-6-thinking
|
|
96
|
+
// "gpt-oss" -> gpt-oss-120b-medium
|
|
89
97
|
const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
|
|
90
|
-
{ full: "
|
|
91
|
-
{ full: "
|
|
92
|
-
{ full: "
|
|
98
|
+
{ full: "claude-sonnet-4-6", family: "other", version: null, tier: null },
|
|
99
|
+
{ full: "claude-opus-4-6-thinking", family: "other", version: null, tier: null },
|
|
100
|
+
{ full: "gpt-oss-120b-medium", family: "other", version: null, tier: null },
|
|
93
101
|
];
|
|
94
102
|
|
|
95
103
|
// Short alias → overlay full string. Used by resolveModel to recognize
|
|
@@ -97,15 +105,15 @@ const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
|
|
|
97
105
|
// not match. The overlay entries are also merged into the live catalog for
|
|
98
106
|
// exact-string passthrough, so this map only needs to cover the short names.
|
|
99
107
|
const STATIC_SHORT_ALIAS: ReadonlyMap<string, string> = new Map([
|
|
100
|
-
["sonnet", "
|
|
101
|
-
["opus", "
|
|
102
|
-
["gpt-oss", "
|
|
108
|
+
["sonnet", "claude-sonnet-4-6"],
|
|
109
|
+
["opus", "claude-opus-4-6-thinking"],
|
|
110
|
+
["gpt-oss", "gpt-oss-120b-medium"],
|
|
103
111
|
]);
|
|
104
112
|
|
|
105
113
|
/** Merge the live catalog with the static alias overlay. Live entries win on
|
|
106
114
|
* case-insensitive full-string equality so an updated `agy models` listing
|
|
107
115
|
* always takes precedence over the hardcoded fallback. */
|
|
108
|
-
function mergeCatalog(live: ModelEntry[]): ModelEntry[] {
|
|
116
|
+
export function mergeCatalog(live: ModelEntry[]): ModelEntry[] {
|
|
109
117
|
const seen = new Set(live.map((e) => e.full.toLowerCase()));
|
|
110
118
|
const merged = [...live];
|
|
111
119
|
for (const entry of STATIC_ALIAS_OVERLAY) {
|
|
@@ -139,13 +147,22 @@ COMPACT OUTPUT (param: digest): when true, the prompt is prefixed to request com
|
|
|
139
147
|
type ThinkingTier = "low" | "medium" | "high";
|
|
140
148
|
type Family = "flash" | "pro" | "other";
|
|
141
149
|
|
|
142
|
-
interface ModelEntry {
|
|
143
|
-
full: string; // exact agy
|
|
150
|
+
export interface ModelEntry {
|
|
151
|
+
full: string; // exact agy slug, e.g. "gemini-3.6-flash-medium"
|
|
144
152
|
family: Family;
|
|
145
|
-
version: string | null; // "3.
|
|
153
|
+
version: string | null; // "3.6"
|
|
146
154
|
tier: ThinkingTier | null;
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
/** Argv-facing model resolution: the exact --model slug plus an optional
|
|
158
|
+
* --effort tier. Gemini bases split the tier out (the base slug alone is
|
|
159
|
+
* invalid without --effort); fixed-thinking families keep agy's exact slug
|
|
160
|
+
* and carry no effort. */
|
|
161
|
+
export interface ResolvedModel {
|
|
162
|
+
model: string;
|
|
163
|
+
effort?: ThinkingTier;
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
interface Config {
|
|
150
167
|
defaultModel: string;
|
|
151
168
|
defaultThinking: ThinkingTier;
|
|
@@ -243,8 +260,11 @@ function saveConfig(patch: Partial<Config>): SaveResult {
|
|
|
243
260
|
// --- Model parsing + alias resolution --------------------------------------
|
|
244
261
|
|
|
245
262
|
/** Parse one `agy models` line into a structured entry. */
|
|
246
|
-
function parseModelLine(line: string): ModelEntry | null {
|
|
247
|
-
|
|
263
|
+
export function parseModelLine(line: string): ModelEntry | null {
|
|
264
|
+
// agy prints TWO columns: "<slug> <display label>". --model takes only the
|
|
265
|
+
// slug (col 1), so split it off; the label is display-only. A bare-slug line
|
|
266
|
+
// (no whitespace) splits to itself.
|
|
267
|
+
const full = line.trim().split(/\s+/)[0] ?? "";
|
|
248
268
|
if (!full) return null;
|
|
249
269
|
|
|
250
270
|
const lower = full.toLowerCase();
|
|
@@ -257,7 +277,7 @@ function parseModelLine(line: string): ModelEntry | null {
|
|
|
257
277
|
const versionMatch = lower.match(/(\d+\.\d+)/);
|
|
258
278
|
const version = versionMatch ? versionMatch[1] : null;
|
|
259
279
|
|
|
260
|
-
const tierMatch = lower.match(
|
|
280
|
+
const tierMatch = lower.match(/-(low|medium|high)$/);
|
|
261
281
|
const tier = tierMatch ? (tierMatch[1] as ThinkingTier) : null;
|
|
262
282
|
|
|
263
283
|
return { full, family, version, tier };
|
|
@@ -276,21 +296,34 @@ function nearestTier(available: ThinkingTier[], preferred: ThinkingTier): Thinki
|
|
|
276
296
|
return sorted[0] ?? preferred;
|
|
277
297
|
}
|
|
278
298
|
|
|
299
|
+
/** Build the argv-facing resolution from a picked catalog entry. Gemini bases
|
|
300
|
+
* (slugs starting "gemini-") accept a separate --effort, so split the tier
|
|
301
|
+
* suffix out of the slug: the base alone (gemini-3.6-flash) is what --model
|
|
302
|
+
* wants, and the tier goes to --effort. Fixed-thinking families keep agy's
|
|
303
|
+
* exact slug even when it carries a -medium suffix (gpt-oss-120b-medium):
|
|
304
|
+
* agy rejects --effort for them, so the suffix stays part of the slug. */
|
|
305
|
+
function toResolved(full: string, tier: ThinkingTier | null): ResolvedModel {
|
|
306
|
+
if (tier && full.toLowerCase().startsWith("gemini-")) {
|
|
307
|
+
return { model: full.replace(/-(low|medium|high)$/, ""), effort: tier };
|
|
308
|
+
}
|
|
309
|
+
return { model: full };
|
|
310
|
+
}
|
|
311
|
+
|
|
279
312
|
/**
|
|
280
|
-
* Resolve a friendly alias / partial name to an
|
|
281
|
-
* Returns null
|
|
282
|
-
*
|
|
313
|
+
* Resolve a friendly alias / partial name to an argv-facing {model, effort?}.
|
|
314
|
+
* Returns null only when the family is unrecognized; the caller then passes
|
|
315
|
+
* the raw input straight to agy.
|
|
283
316
|
*/
|
|
284
|
-
function resolveModel(
|
|
317
|
+
export function resolveModel(
|
|
285
318
|
input: string,
|
|
286
319
|
entries: ModelEntry[],
|
|
287
320
|
defaultThinking: ThinkingTier,
|
|
288
|
-
):
|
|
321
|
+
): ResolvedModel | null {
|
|
289
322
|
const lower = input.toLowerCase().trim();
|
|
290
323
|
|
|
291
324
|
// 1. Exact full-string match (case-insensitive).
|
|
292
325
|
const exact = entries.find((e) => e.full.toLowerCase() === lower);
|
|
293
|
-
if (exact) return exact.full;
|
|
326
|
+
if (exact) return toResolved(exact.full, exact.tier);
|
|
294
327
|
|
|
295
328
|
// 1b. Static short alias ("sonnet" / "opus" / "gpt-oss"). Checked
|
|
296
329
|
// before the family parser because none of these names contain
|
|
@@ -303,9 +336,8 @@ function resolveModel(
|
|
|
303
336
|
// the model under different casing than the overlay.
|
|
304
337
|
if (STATIC_SHORT_ALIAS.has(lower)) {
|
|
305
338
|
const target = STATIC_SHORT_ALIAS.get(lower) as string;
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
return fromCatalog ? fromCatalog.full : target;
|
|
339
|
+
const fromCatalog = entries.find((e) => e.full.toLowerCase() === target.toLowerCase());
|
|
340
|
+
return toResolved(fromCatalog?.full ?? target, fromCatalog?.tier ?? null);
|
|
309
341
|
}
|
|
310
342
|
|
|
311
343
|
// 2. Parse the alias.
|
|
@@ -365,13 +397,14 @@ function resolveModel(
|
|
|
365
397
|
const familyTiers = new Set(
|
|
366
398
|
candidates.map((e) => e.tier).filter((t): t is ThinkingTier => t !== null),
|
|
367
399
|
);
|
|
368
|
-
if (familyTiers.size === 0) return candidates[0].full; // no tiers on any entry
|
|
400
|
+
if (familyTiers.size === 0) return toResolved(candidates[0].full, null); // no tiers on any entry
|
|
369
401
|
|
|
370
402
|
const preferred =
|
|
371
403
|
tier ??
|
|
372
404
|
(familyTiers.has(defaultThinking) ? defaultThinking : FAMILY_DEFAULT_TIER[family]);
|
|
373
405
|
const chosenTier = nearestTier([...familyTiers], preferred);
|
|
374
|
-
|
|
406
|
+
const picked = candidates.find((e) => e.tier === chosenTier) ?? candidates[0];
|
|
407
|
+
return toResolved(picked.full, picked.tier);
|
|
375
408
|
}
|
|
376
409
|
|
|
377
410
|
// --- agy process helpers ---------------------------------------------------
|
|
@@ -620,14 +653,82 @@ function newConversationId(
|
|
|
620
653
|
return null;
|
|
621
654
|
}
|
|
622
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
|
+
|
|
623
722
|
// --- Extension -------------------------------------------------------------
|
|
624
723
|
|
|
625
724
|
interface AgyDetails {
|
|
626
725
|
model: string | null;
|
|
627
726
|
resolvedModel: string | null;
|
|
727
|
+
thinking: ThinkingTier | null;
|
|
628
728
|
mode: Mode;
|
|
629
729
|
digest: boolean;
|
|
630
730
|
conversationId: string | null;
|
|
731
|
+
includeContext: boolean;
|
|
631
732
|
exitCode: number;
|
|
632
733
|
aborted: boolean;
|
|
633
734
|
timedOut: boolean;
|
|
@@ -671,7 +772,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
671
772
|
` defaultModel: ${config.defaultModel}`,
|
|
672
773
|
` defaultThinking: ${config.defaultThinking}`,
|
|
673
774
|
` permissions: ${config.skipPermissions ? "auto-approved" : "prompt"}`,
|
|
674
|
-
` resolved: ${resolveModel(config.defaultModel, discovered, config.defaultThinking) ?? "(agy default)"}`,
|
|
775
|
+
` resolved: ${resolveModel(config.defaultModel, discovered, config.defaultThinking)?.model ?? "(agy default)"}`,
|
|
675
776
|
``,
|
|
676
777
|
`Edit: ~/.pi/agent/ask-antigravity.json`,
|
|
677
778
|
].join("\n"),
|
|
@@ -682,7 +783,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
682
783
|
|
|
683
784
|
// Resolve the display string for the current default model.
|
|
684
785
|
const currentResolved =
|
|
685
|
-
resolveModel(config.defaultModel, discovered, config.defaultThinking) ?? config.defaultModel;
|
|
786
|
+
resolveModel(config.defaultModel, discovered, config.defaultThinking)?.model ?? config.defaultModel;
|
|
686
787
|
|
|
687
788
|
const items: SettingItem[] = [
|
|
688
789
|
{
|
|
@@ -692,7 +793,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
692
793
|
"Friendly alias resolved to the latest matching agy model. 'flash' = latest Flash, 'pro' = latest Pro, 'gemini' = latest Flash.",
|
|
693
794
|
currentValue: `${config.defaultModel} → ${currentResolved}`,
|
|
694
795
|
values: MODEL_OPTIONS.map((m) => {
|
|
695
|
-
const r = resolveModel(m, discovered, config.defaultThinking) ?? m;
|
|
796
|
+
const r = resolveModel(m, discovered, config.defaultThinking)?.model ?? m;
|
|
696
797
|
return `${m} → ${r}`;
|
|
697
798
|
}),
|
|
698
799
|
},
|
|
@@ -830,7 +931,80 @@ export default async function (pi: ExtensionAPI) {
|
|
|
830
931
|
description: `Hard cap on the agy run in minutes. Default ${DEFAULT_TIMEOUT_MIN}.`,
|
|
831
932
|
}),
|
|
832
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
|
+
),
|
|
833
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
|
+
},
|
|
834
1008
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
835
1009
|
// Circular-delegation guard (best-effort). This extension registers
|
|
836
1010
|
// NO provider, so the check only fires if a future agy-as-provider
|
|
@@ -877,7 +1051,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
877
1051
|
};
|
|
878
1052
|
}
|
|
879
1053
|
const resolved =
|
|
880
|
-
resolveModel(requestedModel, discovered, config.defaultThinking) ??
|
|
1054
|
+
resolveModel(requestedModel, discovered, config.defaultThinking) ?? {
|
|
1055
|
+
model: requestedModel,
|
|
1056
|
+
};
|
|
881
1057
|
|
|
882
1058
|
const start = Date.now();
|
|
883
1059
|
const cwd = params.cwd || ctx.cwd || process.cwd();
|
|
@@ -888,13 +1064,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
888
1064
|
if (!stat.isDirectory()) {
|
|
889
1065
|
return {
|
|
890
1066
|
content: [{ type: "text", text: `cwd is not a directory: ${cwd}` }],
|
|
891
|
-
details: emptyDetails(requestedModel, resolved),
|
|
1067
|
+
details: emptyDetails(requestedModel, resolved.model),
|
|
892
1068
|
};
|
|
893
1069
|
}
|
|
894
1070
|
} catch {
|
|
895
1071
|
return {
|
|
896
1072
|
content: [{ type: "text", text: `cwd does not exist: ${cwd}` }],
|
|
897
|
-
details: emptyDetails(requestedModel, resolved),
|
|
1073
|
+
details: emptyDetails(requestedModel, resolved.model),
|
|
898
1074
|
};
|
|
899
1075
|
}
|
|
900
1076
|
|
|
@@ -924,10 +1100,34 @@ export default async function (pi: ExtensionAPI) {
|
|
|
924
1100
|
? `(Use compact digests, not full file contents.)\n${params.prompt}`
|
|
925
1101
|
: params.prompt;
|
|
926
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
|
+
|
|
927
1126
|
const args: string[] = ["--add-dir", cwd];
|
|
928
1127
|
const extra = extraArgs();
|
|
929
1128
|
if (extra.length) args.push(...extra);
|
|
930
|
-
if (resolved) args.push("--model", resolved);
|
|
1129
|
+
if (resolved.model) args.push("--model", resolved.model);
|
|
1130
|
+
if (resolved.effort) args.push("--effort", resolved.effort);
|
|
931
1131
|
args.push("--mode", mode);
|
|
932
1132
|
// accept-edits auto-approves file edits but NOT shell commands, so a
|
|
933
1133
|
// run_command would hang on an unanswerable y/n prompt in non-interactive
|
|
@@ -935,14 +1135,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
935
1135
|
if (config.skipPermissions) args.push("--dangerously-skip-permissions");
|
|
936
1136
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
937
1137
|
args.push("--print-timeout", `${timeoutMin}m`);
|
|
938
|
-
args.push("-
|
|
1138
|
+
if (contextFile) args.push("--add-dir", askContextDir());
|
|
1139
|
+
args.push("-p", effectivePrompt);
|
|
939
1140
|
|
|
940
1141
|
const details: AgyDetails = {
|
|
941
1142
|
model: requestedModel,
|
|
942
|
-
resolvedModel: resolved,
|
|
1143
|
+
resolvedModel: resolved.model,
|
|
1144
|
+
thinking: resolved.effort ?? config.defaultThinking,
|
|
943
1145
|
mode,
|
|
944
1146
|
digest: useDigest,
|
|
945
1147
|
conversationId: isContinuation ? (rawConvId as string) : null,
|
|
1148
|
+
includeContext: contextFile !== null,
|
|
946
1149
|
exitCode: 0,
|
|
947
1150
|
aborted: false,
|
|
948
1151
|
timedOut: false,
|
|
@@ -1183,16 +1386,30 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1183
1386
|
details,
|
|
1184
1387
|
};
|
|
1185
1388
|
}
|
|
1389
|
+
finally {
|
|
1390
|
+
if (contextFile) {
|
|
1391
|
+
try {
|
|
1392
|
+
fs.unlinkSync(contextFile);
|
|
1393
|
+
} catch {}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1186
1396
|
},
|
|
1187
1397
|
});
|
|
1188
1398
|
}
|
|
1189
1399
|
|
|
1190
|
-
function emptyDetails(
|
|
1400
|
+
function emptyDetails(
|
|
1401
|
+
model: string | null,
|
|
1402
|
+
resolvedModel: string | null,
|
|
1403
|
+
thinking: ThinkingTier | null = null,
|
|
1404
|
+
includeContext: boolean = false,
|
|
1405
|
+
): AgyDetails {
|
|
1191
1406
|
return {
|
|
1192
1407
|
model,
|
|
1193
1408
|
resolvedModel,
|
|
1409
|
+
thinking,
|
|
1194
1410
|
mode: "accept-edits",
|
|
1195
1411
|
digest: false,
|
|
1412
|
+
includeContext,
|
|
1196
1413
|
conversationId: null,
|
|
1197
1414
|
exitCode: 0,
|
|
1198
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
|
+
"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",
|