@mrclrchtr/supi-antigravity 6.4.0 → 7.0.1
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/README.md +2 -2
- package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -5
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/package.json +2 -7
- package/src/catalogue.ts +2 -1
- package/src/extension.ts +5 -2
- package/src/footer-constants.ts +5 -0
- package/src/footer.ts +27 -0
- package/src/runtime.ts +125 -4
- package/src/tool/antigravity_run/guidance.ts +1 -1
- package/src/tool/antigravity_run/input.ts +33 -22
package/README.md
CHANGED
|
@@ -16,14 +16,14 @@ Run `/reload` after installation.
|
|
|
16
16
|
|
|
17
17
|
The package never uses the normal Antigravity profile. It creates an Isolated Antigravity Home under the Pi agent directory and a stable empty Consultation Workspace.
|
|
18
18
|
|
|
19
|
-
Before the tool can appear, Pi shows a command like this:
|
|
19
|
+
After Pi starts, the package checks availability without making startup wait. The footer shows a spinning icon while the check runs. The tool appears only after the check is complete. When it is ready, the footer settles on the terminal-safe `✦` icon. With the SuPi footer, it appears on the stats line as `| ✦`. Before the tool can appear, Pi shows a command like this:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
cd "<consultation-workspace>" &&
|
|
23
23
|
HOME="<isolated-home>" AGY_CLI_DISABLE_AUTO_UPDATE=true agy
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
On macOS, the package also creates and unlocks a private keychain inside the Isolated Antigravity Home. This avoids the macOS warning about a missing default keychain without prompting for the normal keychain password. Run the command, sign in, exit Antigravity, and reload Pi. The installed `agy` version must be at least `1.1.24`.
|
|
26
|
+
On macOS, the package also creates and unlocks a private keychain inside the Isolated Antigravity Home. This avoids the macOS warning about a missing default keychain without prompting for the normal keychain password. Run the command, sign in, exit Antigravity, and reload Pi. The installed `agy` version must be at least `1.1.24`. The availability check can still be running when Pi becomes ready.
|
|
27
27
|
|
|
28
28
|
If macOS displays `antigravity.` in the warning, the final `.` is sentence punctuation. It is not part of a keychain name.
|
|
29
29
|
|
|
@@ -21,6 +21,7 @@ pnpm add @mrclrchtr/supi-core
|
|
|
21
21
|
## Package surfaces
|
|
22
22
|
|
|
23
23
|
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
24
|
+
- `@mrclrchtr/supi-core/llm` — PI-owned direct model requests and JSON helpers
|
|
24
25
|
- `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
|
|
25
26
|
|
|
26
27
|
## What you get from the API
|
|
@@ -48,6 +49,13 @@ Config file locations:
|
|
|
48
49
|
|
|
49
50
|
- `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
|
|
50
51
|
|
|
52
|
+
### Model requests
|
|
53
|
+
|
|
54
|
+
- `completeModelRequest(ctx, model, context, options)` — complete through PI's model registry with stable feature affinity. PI owns auth and endpoint resolution.
|
|
55
|
+
- `callWithJsonResponse()` — retry a registry request, extract JSON, and validate it with TypeBox.
|
|
56
|
+
|
|
57
|
+
`completeModelRequest()` requires a stable `affinityScope`. It keeps cache retention defaults, does not include prompt content in the affinity ID, and adds OpenCode headers only when the provider or exact model endpoint matches OpenCode. Pass `maxTokens: model.maxTokens` when a caller needs the model's declared output cap without using PI private modules.
|
|
58
|
+
|
|
51
59
|
### Shared registries
|
|
52
60
|
|
|
53
61
|
- context-provider registry for `/supi-context`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"description": "Shared settings, configuration, reporting, and session infrastructure",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,10 +48,6 @@
|
|
|
48
48
|
"optional": true
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@types/node": "25.9.5",
|
|
53
|
-
"vitest": "4.1.11"
|
|
54
|
-
},
|
|
55
51
|
"main": "src/api.ts",
|
|
56
52
|
"exports": {
|
|
57
53
|
"./api": "./src/api.ts",
|
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Extensions register pre-styled text chunks with a placement hint
|
|
4
4
|
// ("stats" for the metrics line, "status" for the extension status line).
|
|
5
|
-
// The custom footer in supi-extras
|
|
6
|
-
//
|
|
5
|
+
// The custom footer in supi-extras reads these contributions and renders them
|
|
6
|
+
// alongside the built-in metrics. Extensions can use PI's status API as a
|
|
7
|
+
// fallback when the custom footer is not installed.
|
|
7
8
|
|
|
8
9
|
import { createRegistry } from "./registry-utils.ts";
|
|
9
10
|
|
|
11
|
+
/** Event emitted when a dynamic footer contribution needs a new render. */
|
|
12
|
+
export const FOOTER_INVALIDATE_EVENT = "supi:footer:invalidate";
|
|
13
|
+
|
|
10
14
|
/** Where the contribution should appear in the footer. */
|
|
11
15
|
export type FooterPlacement = "stats" | "stats-end" | "status";
|
|
12
16
|
|
|
13
17
|
/** A single footer contribution registered by an extension. */
|
|
14
18
|
export interface FooterContribution {
|
|
15
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Unique key for this contribution. Re-registering with the same key replaces it.
|
|
21
|
+
* A same-key Pi status is treated as this contribution's built-in-footer fallback.
|
|
22
|
+
*/
|
|
16
23
|
key: string;
|
|
17
24
|
/** Which footer line this belongs on. */
|
|
18
25
|
placement: FooterPlacement;
|
|
@@ -15,6 +15,8 @@ export * from "./debug.ts";
|
|
|
15
15
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
16
|
export * from "./footer-registry.ts";
|
|
17
17
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
18
|
+
export * from "./llm.ts";
|
|
19
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
18
20
|
export * from "./model-selection.ts";
|
|
19
21
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
20
22
|
export * from "./path.ts";
|
|
@@ -1,12 +1,131 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
Api,
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
Context,
|
|
6
|
+
Model,
|
|
7
|
+
ModelsApiStreamOptions,
|
|
8
|
+
ProviderHeaders,
|
|
9
|
+
} from "@earendil-works/pi-ai";
|
|
2
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
11
|
import type { TSchema } from "typebox";
|
|
4
12
|
import { Value } from "typebox/value";
|
|
5
13
|
|
|
6
14
|
// Shared LLM utilities for SuPi extensions.
|
|
7
15
|
//
|
|
8
|
-
// Provides retry logic, structured LLM call helpers,
|
|
9
|
-
// common patterns for extensions that interact with AI models.
|
|
16
|
+
// Provides PI-owned model requests, retry logic, structured LLM call helpers,
|
|
17
|
+
// and other common patterns for extensions that interact with AI models.
|
|
18
|
+
|
|
19
|
+
const MODEL_REQUEST_NAMESPACE = "supi-direct-model-request-v1";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for {@link completeModelRequest}.
|
|
23
|
+
*
|
|
24
|
+
* Authentication, provider environment, and session identity stay under PI
|
|
25
|
+
* control. The feature supplies a stable scope for its prompt stream.
|
|
26
|
+
*/
|
|
27
|
+
export type CompleteModelRequestOptions<TApi extends Api = Api> = Omit<
|
|
28
|
+
ModelsApiStreamOptions<TApi>,
|
|
29
|
+
"apiKey" | "env" | "sessionId"
|
|
30
|
+
> & {
|
|
31
|
+
/** Stable feature scope. Do not include prompt, turn, or retry data. */
|
|
32
|
+
affinityScope: string;
|
|
33
|
+
/** PI owns these fields, including for APIs with open-ended option types. */
|
|
34
|
+
apiKey?: never;
|
|
35
|
+
env?: never;
|
|
36
|
+
sessionId?: never;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function createModelRequestAffinityId(
|
|
40
|
+
sessionId: string,
|
|
41
|
+
affinityScope: string,
|
|
42
|
+
model: Model<Api>,
|
|
43
|
+
): string {
|
|
44
|
+
const material = JSON.stringify([
|
|
45
|
+
MODEL_REQUEST_NAMESPACE,
|
|
46
|
+
sessionId,
|
|
47
|
+
affinityScope,
|
|
48
|
+
model.provider,
|
|
49
|
+
model.id,
|
|
50
|
+
]);
|
|
51
|
+
const digest = createHash("sha256").update(material, "utf8").digest("hex");
|
|
52
|
+
return `supi-${digest.slice(0, 56)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isOpenCodeModel(model: Model<Api>): boolean {
|
|
56
|
+
if (model.provider === "opencode" || model.provider === "opencode-go") return true;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
return new URL(model.baseUrl).hostname === "opencode.ai";
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasHeader(headers: ProviderHeaders, name: string): boolean {
|
|
66
|
+
const lowerName = name.toLowerCase();
|
|
67
|
+
return Object.keys(headers).some((headerName) => headerName.toLowerCase() === lowerName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addOpenCodeDefaultHeaders(
|
|
71
|
+
model: Model<Api>,
|
|
72
|
+
affinityId: string,
|
|
73
|
+
headers: ProviderHeaders,
|
|
74
|
+
): ProviderHeaders {
|
|
75
|
+
if (!isOpenCodeModel(model)) return headers;
|
|
76
|
+
|
|
77
|
+
const result = { ...headers };
|
|
78
|
+
if (!hasHeader(result, "x-opencode-session")) {
|
|
79
|
+
result["x-opencode-session"] = affinityId;
|
|
80
|
+
}
|
|
81
|
+
if (!hasHeader(result, "x-opencode-client")) {
|
|
82
|
+
result["x-opencode-client"] = "pi";
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Complete a direct request through PI's model registry.
|
|
89
|
+
*
|
|
90
|
+
* PI resolves authentication, provider headers, environment, and the
|
|
91
|
+
* effective endpoint. This helper adds one stable opaque session identity for
|
|
92
|
+
* the feature prompt stream and applies the OpenCode compatibility defaults.
|
|
93
|
+
* It does not retry, validate output, or present errors.
|
|
94
|
+
*
|
|
95
|
+
* When `maxTokens` is omitted, the underlying registry receives no explicit
|
|
96
|
+
* output cap. A caller that needs the selected model's declared cap can pass
|
|
97
|
+
* `maxTokens: model.maxTokens` without importing PI internals.
|
|
98
|
+
*/
|
|
99
|
+
export async function completeModelRequest<TApi extends Api>(
|
|
100
|
+
ctx: ExtensionContext,
|
|
101
|
+
model: Model<TApi>,
|
|
102
|
+
context: Context,
|
|
103
|
+
options: CompleteModelRequestOptions<TApi>,
|
|
104
|
+
): Promise<AssistantMessage> {
|
|
105
|
+
const { affinityScope, transformHeaders: callerTransformHeaders, ...requestOptions } = options;
|
|
106
|
+
const safeRequestOptions = { ...requestOptions };
|
|
107
|
+
delete safeRequestOptions.apiKey;
|
|
108
|
+
delete safeRequestOptions.env;
|
|
109
|
+
delete safeRequestOptions.sessionId;
|
|
110
|
+
|
|
111
|
+
const affinityId = createModelRequestAffinityId(
|
|
112
|
+
ctx.sessionManager.getSessionId(),
|
|
113
|
+
affinityScope,
|
|
114
|
+
model,
|
|
115
|
+
);
|
|
116
|
+
const transformHeaders = async (headers: ProviderHeaders): Promise<ProviderHeaders> => {
|
|
117
|
+
const transformed = callerTransformHeaders ? await callerTransformHeaders(headers) : headers;
|
|
118
|
+
return addOpenCodeDefaultHeaders(model, affinityId, transformed);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Restore PI's conditional provider-option type after removing owned fields.
|
|
122
|
+
return ctx.modelRegistry.complete(model, context, {
|
|
123
|
+
...safeRequestOptions,
|
|
124
|
+
signal: safeRequestOptions.signal ?? ctx.signal,
|
|
125
|
+
sessionId: affinityId,
|
|
126
|
+
transformHeaders,
|
|
127
|
+
} as unknown as ModelsApiStreamOptions<TApi>);
|
|
128
|
+
}
|
|
10
129
|
|
|
11
130
|
/**
|
|
12
131
|
* Options for {@link withRetry}.
|
|
@@ -122,6 +241,8 @@ export function extractJsonFromResponse<T extends TSchema>(
|
|
|
122
241
|
export interface CallWithJsonResponseOptions {
|
|
123
242
|
/** The prompt to send to the LLM. */
|
|
124
243
|
prompt: string;
|
|
244
|
+
/** Stable feature scope for request affinity. Do not include prompt or retry data. */
|
|
245
|
+
affinityScope: string;
|
|
125
246
|
/** Optional data context appended to the prompt. */
|
|
126
247
|
dataContext?: string;
|
|
127
248
|
/** Maximum tokens for the response. Default: 4096 */
|
|
@@ -135,8 +256,9 @@ export interface CallWithJsonResponseOptions {
|
|
|
135
256
|
/**
|
|
136
257
|
* Call the LLM with a prompt and validate the JSON response against a TypeBox schema.
|
|
137
258
|
*
|
|
138
|
-
* Handles model resolution,
|
|
139
|
-
*
|
|
259
|
+
* Handles model resolution, retry via `withRetry`, text extraction, JSON
|
|
260
|
+
* matching, and TypeBox validation. The request itself stays under PI
|
|
261
|
+
* registry authority through {@link completeModelRequest}.
|
|
140
262
|
*
|
|
141
263
|
* Returns `null` when:
|
|
142
264
|
* - No model is available
|
|
@@ -145,7 +267,7 @@ export interface CallWithJsonResponseOptions {
|
|
|
145
267
|
* - JSON doesn't match the schema
|
|
146
268
|
* - The request is aborted
|
|
147
269
|
*
|
|
148
|
-
* @param ctx - The extension context for model
|
|
270
|
+
* @param ctx - The extension context for model selection and PI registry access.
|
|
149
271
|
* @param options - Call options including prompt, schema, and retry config.
|
|
150
272
|
* @param schema - TypeBox schema to validate the JSON response against.
|
|
151
273
|
* @returns The parsed and validated result, or `null`.
|
|
@@ -155,14 +277,18 @@ export async function callWithJsonResponse<T extends TSchema>(
|
|
|
155
277
|
options: CallWithJsonResponseOptions,
|
|
156
278
|
schema: T,
|
|
157
279
|
): Promise<{ parsed: import("typebox").Static<T> } | null> {
|
|
158
|
-
const {
|
|
280
|
+
const {
|
|
281
|
+
prompt,
|
|
282
|
+
affinityScope,
|
|
283
|
+
dataContext,
|
|
284
|
+
maxTokens = 4096,
|
|
285
|
+
systemPrompt = "",
|
|
286
|
+
retries = 2,
|
|
287
|
+
} = options;
|
|
159
288
|
|
|
160
289
|
const model = ctx.model ?? ctx.modelRegistry.getAvailable()[0] ?? null;
|
|
161
290
|
if (!model) return null;
|
|
162
291
|
|
|
163
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
164
|
-
if (!auth.ok || !auth.apiKey) return null;
|
|
165
|
-
|
|
166
292
|
const fullPrompt = dataContext
|
|
167
293
|
? `${prompt}
|
|
168
294
|
|
|
@@ -171,8 +297,9 @@ ${dataContext}`
|
|
|
171
297
|
: prompt;
|
|
172
298
|
|
|
173
299
|
const response = await withRetry(
|
|
174
|
-
async () =>
|
|
175
|
-
|
|
300
|
+
async () =>
|
|
301
|
+
completeModelRequest(
|
|
302
|
+
ctx,
|
|
176
303
|
model,
|
|
177
304
|
{
|
|
178
305
|
systemPrompt,
|
|
@@ -185,13 +312,11 @@ ${dataContext}`
|
|
|
185
312
|
],
|
|
186
313
|
},
|
|
187
314
|
{
|
|
188
|
-
|
|
189
|
-
headers: auth.headers,
|
|
315
|
+
affinityScope,
|
|
190
316
|
signal: ctx.signal,
|
|
191
317
|
maxTokens,
|
|
192
318
|
},
|
|
193
|
-
)
|
|
194
|
-
},
|
|
319
|
+
),
|
|
195
320
|
{ retries, baseDelayMs: 1000, signal: ctx.signal },
|
|
196
321
|
);
|
|
197
322
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-antigravity",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"description": "Bounded Antigravity tasks with isolated workspace access and evidence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"docs/adr/*.md"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@mrclrchtr/supi-core": "
|
|
39
|
+
"@mrclrchtr/supi-core": "7.0.1"
|
|
40
40
|
},
|
|
41
41
|
"bundledDependencies": [
|
|
42
42
|
"@mrclrchtr/supi-core"
|
|
@@ -61,11 +61,6 @@
|
|
|
61
61
|
"optional": true
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@mrclrchtr/supi-test-utils": "workspace:*",
|
|
66
|
-
"@types/node": "25.9.5",
|
|
67
|
-
"vitest": "4.1.11"
|
|
68
|
-
},
|
|
69
64
|
"pi": {
|
|
70
65
|
"extensions": [
|
|
71
66
|
"./src/extension.ts"
|
package/src/catalogue.ts
CHANGED
|
@@ -8,7 +8,8 @@ export function buildModelCatalogueEnum(catalogue: readonly CuratedModel[]): TSc
|
|
|
8
8
|
throw new Error("Cannot build an Antigravity model enum from an empty catalogue.");
|
|
9
9
|
}
|
|
10
10
|
return StringEnum([...catalogue] as [string, ...string[]], {
|
|
11
|
-
description:
|
|
11
|
+
description:
|
|
12
|
+
"Curated Antigravity model available to the current account; new runs have no default.",
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
|
package/src/extension.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerAntigravityFooterContribution } from "./footer.ts";
|
|
2
3
|
import { AntigravityRuntime } from "./runtime.ts";
|
|
3
4
|
import { registerAntigravitySettings } from "./settings.ts";
|
|
4
5
|
|
|
5
6
|
/** Register the opt-in Antigravity Run extension. */
|
|
6
7
|
export default function antigravityExtension(pi: ExtensionAPI): void {
|
|
7
8
|
const runtime = new AntigravityRuntime({ pi });
|
|
9
|
+
const footer = registerAntigravityFooterContribution(runtime);
|
|
8
10
|
registerAntigravitySettings(pi, runtime);
|
|
9
11
|
|
|
10
|
-
pi.on("session_start",
|
|
12
|
+
pi.on("session_start", (_event, ctx) => {
|
|
11
13
|
runtime.rebuildHandles(ctx.sessionManager.getBranch());
|
|
12
|
-
|
|
14
|
+
void runtime.startRefresh(ctx.cwd, ctx);
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
pi.on("session_tree", (_event, ctx) => {
|
|
@@ -18,5 +20,6 @@ export default function antigravityExtension(pi: ExtensionAPI): void {
|
|
|
18
20
|
|
|
19
21
|
pi.on("session_shutdown", async () => {
|
|
20
22
|
await runtime.shutdown();
|
|
23
|
+
footer.dispose();
|
|
21
24
|
});
|
|
22
25
|
}
|
package/src/footer.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { footerContributions } from "@mrclrchtr/supi-core/footer-registry";
|
|
2
|
+
import { ANTIGRAVITY_FOOTER_KEY } from "./footer-constants.ts";
|
|
3
|
+
import type { AntigravityRuntime } from "./runtime.ts";
|
|
4
|
+
|
|
5
|
+
/** Register the Antigravity checking spinner and ready icon on the footer stats line. */
|
|
6
|
+
export function registerAntigravityFooterContribution(runtime: AntigravityRuntime): {
|
|
7
|
+
dispose: () => void;
|
|
8
|
+
} {
|
|
9
|
+
footerContributions.register({
|
|
10
|
+
key: ANTIGRAVITY_FOOTER_KEY,
|
|
11
|
+
placement: "stats-end",
|
|
12
|
+
priority: 110,
|
|
13
|
+
render: () => {
|
|
14
|
+
const icon = runtime.footerIcon;
|
|
15
|
+
return icon ? `| ${icon}` : "";
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
dispose: unregisterAntigravityFooterContribution,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Remove the Antigravity ready icon from the footer. */
|
|
25
|
+
export function unregisterAntigravityFooterContribution(): void {
|
|
26
|
+
footerContributions.unregister(ANTIGRAVITY_FOOTER_KEY);
|
|
27
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { FOOTER_INVALIDATE_EVENT } from "@mrclrchtr/supi-core/footer-registry";
|
|
3
|
+
import { BRAILLE_SPINNER_FRAMES, SPINNER_INTERVAL_MS } from "@mrclrchtr/supi-core/spinner-frames";
|
|
2
4
|
import { type AntigravityAvailability, discoverAntigravityAvailability } from "./availability.ts";
|
|
3
5
|
import { loadAntigravityConfig } from "./config.ts";
|
|
4
6
|
import { ConversationHandleStore } from "./conversation/handles.ts";
|
|
7
|
+
import { ANTIGRAVITY_FOOTER_KEY, ANTIGRAVITY_READY_ICON } from "./footer-constants.ts";
|
|
5
8
|
import { getIsolatedAntigravityPaths, type IsolatedAntigravityPaths } from "./isolated-home.ts";
|
|
6
9
|
import { registerAntigravityRunTool } from "./tool/antigravity_run/register.ts";
|
|
7
10
|
import { ANTIGRAVITY_RUN_TOOL_NAME } from "./tool/antigravity_run/spec.ts";
|
|
8
11
|
import type { CuratedModel } from "./types.ts";
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
type FooterState = "idle" | "checking" | "ready";
|
|
14
|
+
|
|
15
|
+
/** Context needed to report availability and update the footer. */
|
|
16
|
+
export type AntigravityRefreshContext = {
|
|
17
|
+
ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
|
|
18
|
+
};
|
|
12
19
|
|
|
13
20
|
/** Session runtime for immutable availability and Conversation Handle state. */
|
|
14
21
|
export class AntigravityRuntime {
|
|
@@ -20,6 +27,10 @@ export class AntigravityRuntime {
|
|
|
20
27
|
#availability: AntigravityAvailability | undefined;
|
|
21
28
|
#refreshGeneration = 0;
|
|
22
29
|
#refreshAbort: AbortController | undefined;
|
|
30
|
+
#statusUi: Pick<ExtensionContext["ui"], "setStatus"> | undefined;
|
|
31
|
+
#footerState: FooterState = "idle";
|
|
32
|
+
#spinnerTimer: ReturnType<typeof setInterval> | undefined;
|
|
33
|
+
#spinnerFrame = 0;
|
|
23
34
|
#toolRegistered = false;
|
|
24
35
|
|
|
25
36
|
constructor(options: {
|
|
@@ -39,22 +50,52 @@ export class AntigravityRuntime {
|
|
|
39
50
|
return this.#availability;
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
/** Whether the discovered Antigravity tool is ready for use. */
|
|
54
|
+
get isReady(): boolean {
|
|
55
|
+
return this.#footerState === "ready";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Return the animated checking icon or the settled ready icon for the footer. */
|
|
59
|
+
get footerIcon(): string | undefined {
|
|
60
|
+
if (this.#footerState === "checking") {
|
|
61
|
+
return BRAILLE_SPINNER_FRAMES[this.#spinnerFrame % BRAILLE_SPINNER_FRAMES.length];
|
|
62
|
+
}
|
|
63
|
+
return this.#footerState === "ready" ? ANTIGRAVITY_READY_ICON : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
/** Rebuild handles from the current PI branch. */
|
|
43
67
|
rebuildHandles(branch: readonly unknown[]): void {
|
|
44
68
|
this.handles.rebuild(branch);
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
/** Start availability discovery without making Pi session startup wait. */
|
|
72
|
+
startRefresh(cwd: string, context?: AntigravityRefreshContext): Promise<void> {
|
|
73
|
+
return this.refresh(cwd, context).catch((error) => {
|
|
74
|
+
try {
|
|
75
|
+
// biome-ignore lint/suspicious/noConsole: unexpected failures must stay visible.
|
|
76
|
+
console.warn(`[supi-antigravity] Availability check failed: ${formatRefreshError(error)}`);
|
|
77
|
+
context?.ui.notify("Antigravity availability check failed. Reload PI to retry.", "warning");
|
|
78
|
+
} catch {
|
|
79
|
+
// PI may be shutting down while the refresh completes.
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
47
84
|
/** Discover or reuse availability and synchronize the active tool. */
|
|
48
85
|
async refresh(cwd: string, context?: AntigravityRefreshContext): Promise<void> {
|
|
49
86
|
const generation = ++this.#refreshGeneration;
|
|
50
87
|
this.#refreshAbort?.abort();
|
|
51
88
|
const abortController = new AbortController();
|
|
52
89
|
this.#refreshAbort = abortController;
|
|
90
|
+
if (context) this.#statusUi = context.ui;
|
|
91
|
+
this.#stopSpinner();
|
|
92
|
+
this.#setFooterState("idle");
|
|
53
93
|
const config = loadAntigravityConfig(cwd, this.#homeDir);
|
|
54
94
|
if (!config.agentToolEnabled) {
|
|
55
95
|
this.#deactivateTool();
|
|
56
96
|
return;
|
|
57
97
|
}
|
|
98
|
+
this.#startSpinner();
|
|
58
99
|
|
|
59
100
|
let availability = this.#availability;
|
|
60
101
|
if (!availability) {
|
|
@@ -75,11 +116,21 @@ export class AntigravityRuntime {
|
|
|
75
116
|
if (generation !== this.#refreshGeneration || abortController.signal.aborted) return;
|
|
76
117
|
this.#availability = availability;
|
|
77
118
|
if (availability.status === "available") {
|
|
78
|
-
|
|
119
|
+
try {
|
|
120
|
+
this.#activateTool(availability.catalogue, availability.cliVersion);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
this.#stopSpinner();
|
|
123
|
+
this.#setFooterState("idle");
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
this.#stopSpinner();
|
|
127
|
+
this.#setFooterState("ready");
|
|
79
128
|
return;
|
|
80
129
|
}
|
|
130
|
+
this.#stopSpinner();
|
|
131
|
+
this.#setFooterState("idle");
|
|
81
132
|
this.#deactivateTool();
|
|
82
|
-
context
|
|
133
|
+
notifyRefreshWarning(context, availability.warning);
|
|
83
134
|
}
|
|
84
135
|
|
|
85
136
|
/** Stop in-flight discovery and clear session-local state. */
|
|
@@ -87,7 +138,10 @@ export class AntigravityRuntime {
|
|
|
87
138
|
this.#refreshGeneration += 1;
|
|
88
139
|
this.#refreshAbort?.abort();
|
|
89
140
|
this.#refreshAbort = undefined;
|
|
141
|
+
this.#stopSpinner();
|
|
142
|
+
this.#setFooterState("idle");
|
|
90
143
|
this.#deactivateTool();
|
|
144
|
+
this.#statusUi = undefined;
|
|
91
145
|
this.handles.clear();
|
|
92
146
|
await Promise.resolve();
|
|
93
147
|
}
|
|
@@ -115,4 +169,71 @@ export class AntigravityRuntime {
|
|
|
115
169
|
this.#pi.setActiveTools(activeTools.filter((name) => name !== ANTIGRAVITY_RUN_TOOL_NAME));
|
|
116
170
|
}
|
|
117
171
|
}
|
|
172
|
+
|
|
173
|
+
#startSpinner(): void {
|
|
174
|
+
this.#stopSpinner();
|
|
175
|
+
this.#spinnerFrame = 0;
|
|
176
|
+
this.#setFooterState("checking");
|
|
177
|
+
this.#spinnerTimer = setInterval(() => {
|
|
178
|
+
if (this.#footerState !== "checking") return;
|
|
179
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
|
|
180
|
+
this.#publishFooter();
|
|
181
|
+
this.#invalidateFooter();
|
|
182
|
+
}, SPINNER_INTERVAL_MS);
|
|
183
|
+
this.#spinnerTimer.unref?.();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#stopSpinner(): void {
|
|
187
|
+
if (this.#spinnerTimer === undefined) return;
|
|
188
|
+
clearInterval(this.#spinnerTimer);
|
|
189
|
+
this.#spinnerTimer = undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#setFooterState(state: FooterState): void {
|
|
193
|
+
const changed = this.#footerState !== state;
|
|
194
|
+
this.#footerState = state;
|
|
195
|
+
this.#publishFooter();
|
|
196
|
+
if (!changed) return;
|
|
197
|
+
this.#invalidateFooter();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#publishFooter(): void {
|
|
201
|
+
try {
|
|
202
|
+
this.#statusUi?.setStatus(ANTIGRAVITY_FOOTER_KEY, this.footerIcon);
|
|
203
|
+
} catch {
|
|
204
|
+
// PI may be shutting down while the footer status changes.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#invalidateFooter(): void {
|
|
209
|
+
try {
|
|
210
|
+
this.#pi.events.emit(FOOTER_INVALIDATE_EVENT, {});
|
|
211
|
+
} catch {
|
|
212
|
+
// Footer refresh is optional and must not change runtime state.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatRefreshError(error: unknown): string {
|
|
218
|
+
const message =
|
|
219
|
+
error instanceof Error
|
|
220
|
+
? error.message
|
|
221
|
+
: typeof error === "object" &&
|
|
222
|
+
error !== null &&
|
|
223
|
+
"message" in error &&
|
|
224
|
+
typeof error.message === "string"
|
|
225
|
+
? error.message
|
|
226
|
+
: String(error);
|
|
227
|
+
return message.replace(/\s+/g, " ").trim().slice(0, 200) || "unknown error";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function notifyRefreshWarning(
|
|
231
|
+
context: AntigravityRefreshContext | undefined,
|
|
232
|
+
message: string,
|
|
233
|
+
): void {
|
|
234
|
+
try {
|
|
235
|
+
context?.ui.notify(message, "warning");
|
|
236
|
+
} catch {
|
|
237
|
+
// PI may be shutting down while the refresh reports its result.
|
|
238
|
+
}
|
|
118
239
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Selection guidance for the dynamically activated antigravity_run tool. */
|
|
2
2
|
export const toolDescription =
|
|
3
|
-
"Consult an external model for focused web research, workspace analysis, or an independent second opinion.";
|
|
3
|
+
"Consult an external model for focused web research, design advice, workspace analysis, or an independent second opinion. Use direct tools for repository facts that do not need external analysis. Expose the current workspace only when the consultation needs repository evidence.";
|
|
@@ -34,35 +34,46 @@ export function buildAntigravityRunSchema(catalogue: readonly CuratedModel[]): T
|
|
|
34
34
|
maxLength: MAX_PROMPT_CHARS,
|
|
35
35
|
description: "The bounded request to send to Antigravity.",
|
|
36
36
|
});
|
|
37
|
-
|
|
37
|
+
// Keep the root schema an object. Some providers reject a top-level anyOf
|
|
38
|
+
// because function parameters must have `type: "object"`.
|
|
39
|
+
return Type.Object(
|
|
38
40
|
{
|
|
39
41
|
prompt,
|
|
40
|
-
new: Type.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
new: Type.Optional(
|
|
43
|
+
Type.Object(
|
|
44
|
+
{
|
|
45
|
+
workspace: Type.Boolean({
|
|
46
|
+
description:
|
|
47
|
+
"true exposes the current PI workspace; false uses the empty Consultation Workspace. Use true only when repository evidence is needed.",
|
|
48
|
+
}),
|
|
49
|
+
model: buildModelCatalogueEnum(catalogue),
|
|
50
|
+
},
|
|
51
|
+
{ additionalProperties: false },
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
continue: Type.Optional(
|
|
55
|
+
Type.Object(
|
|
56
|
+
{
|
|
57
|
+
handle: Type.String({
|
|
58
|
+
minLength: 1,
|
|
59
|
+
maxLength: MAX_HANDLE_CHARS,
|
|
60
|
+
description: "Conversation Handle returned by antigravity_run.",
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
description: "Continue a prior run and inherit its model and workspace access.",
|
|
66
|
+
},
|
|
67
|
+
),
|
|
49
68
|
),
|
|
50
69
|
},
|
|
51
|
-
{ additionalProperties: false },
|
|
52
|
-
);
|
|
53
|
-
const continueInput = Type.Object(
|
|
54
70
|
{
|
|
55
|
-
prompt,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
},
|
|
60
|
-
{ additionalProperties: false },
|
|
61
|
-
),
|
|
71
|
+
// `prompt` is required, so these bounds require exactly one branch.
|
|
72
|
+
minProperties: 2,
|
|
73
|
+
maxProperties: 2,
|
|
74
|
+
additionalProperties: false,
|
|
62
75
|
},
|
|
63
|
-
{ additionalProperties: false },
|
|
64
76
|
);
|
|
65
|
-
return Type.Union([newInput, continueInput]);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
/** Validate exact-one input and the model against the same catalogue. */
|