pi-ui-extend 0.1.13 → 0.1.17
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 +1 -1
- package/dist/app/app.d.ts +7 -0
- package/dist/app/app.js +102 -17
- package/dist/app/commands/command-controller.js +2 -0
- package/dist/app/commands/command-host.d.ts +5 -0
- package/dist/app/commands/command-model-actions.d.ts +2 -0
- package/dist/app/commands/command-model-actions.js +40 -4
- package/dist/app/commands/command-navigation-actions.d.ts +9 -0
- package/dist/app/commands/command-navigation-actions.js +62 -0
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +16 -0
- package/dist/app/constants.d.ts +0 -1
- package/dist/app/constants.js +0 -1
- package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/logger.d.ts +25 -0
- package/dist/app/logger.js +90 -0
- package/dist/app/model/model-usage-status.js +30 -15
- package/dist/app/popup/menu-items-controller.d.ts +4 -0
- package/dist/app/popup/menu-items-controller.js +68 -6
- package/dist/app/popup/popup-action-controller.d.ts +2 -1
- package/dist/app/popup/popup-action-controller.js +7 -4
- package/dist/app/popup/popup-menu-controller.d.ts +36 -23
- package/dist/app/popup/popup-menu-controller.js +97 -326
- package/dist/app/rendering/conversation-entry-renderer.js +3 -3
- package/dist/app/rendering/conversation-viewport.d.ts +10 -2
- package/dist/app/rendering/conversation-viewport.js +157 -16
- package/dist/app/rendering/editor-panels.js +22 -9
- package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
- package/dist/app/rendering/popup-menu-renderer.js +405 -0
- package/dist/app/rendering/render-controller.js +30 -28
- package/dist/app/rendering/render-text.js +5 -2
- package/dist/app/rendering/status-line-renderer.d.ts +8 -1
- package/dist/app/rendering/status-line-renderer.js +217 -117
- package/dist/app/rendering/toast-controller.d.ts +12 -3
- package/dist/app/rendering/toast-controller.js +70 -12
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +20 -10
- package/dist/app/screen/mouse-controller.d.ts +2 -2
- package/dist/app/screen/mouse-controller.js +27 -48
- package/dist/app/screen/screen-styler.d.ts +1 -1
- package/dist/app/screen/screen-styler.js +9 -7
- package/dist/app/screen/scroll-controller.d.ts +12 -9
- package/dist/app/screen/scroll-controller.js +56 -45
- package/dist/app/screen/status-controller.js +2 -1
- package/dist/app/session/lazy-session-manager.d.ts +11 -0
- package/dist/app/session/lazy-session-manager.js +539 -0
- package/dist/app/session/pix-system-message.d.ts +16 -0
- package/dist/app/session/pix-system-message.js +64 -0
- package/dist/app/session/request-history.d.ts +4 -0
- package/dist/app/session/request-history.js +11 -0
- package/dist/app/session/session-event-controller.d.ts +11 -0
- package/dist/app/session/session-event-controller.js +58 -2
- package/dist/app/session/session-history.d.ts +18 -0
- package/dist/app/session/session-history.js +72 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
- package/dist/app/session/session-lifecycle-controller.js +7 -2
- package/dist/app/session/session-search.js +10 -0
- package/dist/app/session/tabs-controller.d.ts +17 -5
- package/dist/app/session/tabs-controller.js +308 -29
- package/dist/app/todo/todo-model.d.ts +4 -2
- package/dist/app/todo/todo-model.js +23 -13
- package/dist/app/types.d.ts +17 -6
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +12 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +82 -25
- package/dist/default-pix-config.js +4 -0
- package/dist/fuzzy.d.ts +2 -0
- package/dist/fuzzy.js +27 -7
- package/dist/input-editor.d.ts +9 -0
- package/dist/input-editor.js +52 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
- package/dist/schemas/pi-tools-suite-schema.js +1 -0
- package/dist/schemas/pix-schema.d.ts +3 -1
- package/dist/schemas/pix-schema.js +6 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/dist/theme.js +6 -6
- package/dist/ui.d.ts +8 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
- package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
- package/external/pi-tools-suite/src/config.ts +8 -0
- package/external/pi-tools-suite/src/dcp/index.ts +16 -1
- package/external/pi-tools-suite/src/dcp/state.ts +35 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
- package/external/pi-tools-suite/src/todo/index.ts +123 -14
- package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
- package/external/pi-tools-suite/src/todo/todo.ts +12 -23
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
- package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
- package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
- package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +11 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
4
|
-
import { accountFromCredential, clampAccountIndex,
|
|
3
|
+
import { DEFAULT_PROJECT_ID, LOAD_ENDPOINTS, REDIRECT_URI, SCOPES, TOKEN_EXPIRY_SKEW_MS, PROVIDER_ID } from "./constants";
|
|
4
|
+
import { accountFromCredential, clampAccountIndex, encodeApiKey, findMatchingAccountIndex, getAccountProjectId, getAccountRefreshToken, getGoogleOAuthClientCredentials, getPiAuthPath, getStoredAccounts, joinRefresh, readJsonFile, splitRefresh, writeJsonFileSecure } from "./auth-store";
|
|
5
5
|
import { getAntigravityHeaders } from "./headers";
|
|
6
|
-
import
|
|
6
|
+
import { notifyAntigravityLoginFailure } from "./status";
|
|
7
|
+
import type { AntigravityAddAccountResult, AntigravityFailoverCredential, AntigravityLoginCallbacks, GoogleOAuthClientCredentials, OpencodeAntigravityAccount, PiAuthCredential, PiAuthData, RefreshedAntigravityAccount } from "./types";
|
|
7
8
|
|
|
8
9
|
function base64Url(input: Buffer): string {
|
|
9
10
|
return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -25,6 +26,12 @@ function decodeState(state: string): { verifier: string; projectId?: string } {
|
|
|
25
26
|
return { verifier: parsed.verifier, projectId: typeof parsed.projectId === "string" ? parsed.projectId : undefined };
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function assertGoogleOAuthCredentialsConfigured(credentials?: GoogleOAuthClientCredentials): asserts credentials is GoogleOAuthClientCredentials & { clientId: string } {
|
|
30
|
+
if (!credentials?.clientId) {
|
|
31
|
+
throw new Error(`Antigravity Google OAuth client credentials are missing in Pi auth: ${getPiAuthPath()}.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
async function fetchProjectId(accessToken: string): Promise<string | undefined> {
|
|
29
36
|
const headers = {
|
|
30
37
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -90,60 +97,65 @@ function extractOAuthParams(input: string): { code: string; state: string } {
|
|
|
90
97
|
throw new Error("Paste the full localhost callback URL, or code#state.");
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
export async function loginAntigravity(callbacks: AntigravityLoginCallbacks): Promise<OAuthCredentials> {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
export async function loginAntigravity(callbacks: AntigravityLoginCallbacks, options: { authPath?: string } = {}): Promise<OAuthCredentials> {
|
|
101
|
+
try {
|
|
102
|
+
const auth = await readJsonFile<PiAuthData>(options.authPath ?? getPiAuthPath(), {});
|
|
103
|
+
const oauthClient = getGoogleOAuthClientCredentials(auth[PROVIDER_ID]);
|
|
104
|
+
assertGoogleOAuthCredentialsConfigured(oauthClient);
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const { verifier, challenge } = generatePkce();
|
|
107
|
+
const state = encodeState({ verifier });
|
|
108
|
+
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
109
|
+
url.searchParams.set("client_id", oauthClient.clientId);
|
|
110
|
+
url.searchParams.set("response_type", "code");
|
|
111
|
+
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
112
|
+
url.searchParams.set("scope", SCOPES.join(" "));
|
|
113
|
+
url.searchParams.set("code_challenge", challenge);
|
|
114
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
115
|
+
url.searchParams.set("state", state);
|
|
116
|
+
url.searchParams.set("access_type", "offline");
|
|
117
|
+
url.searchParams.set("prompt", "consent");
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
callbacks.onAuth({ url: url.toString() });
|
|
120
|
+
const pasted = await callbacks.onPrompt({
|
|
121
|
+
message: "Paste the full http://localhost:51121/oauth-callback URL after Google login (or code#state):",
|
|
122
|
+
});
|
|
123
|
+
const params = extractOAuthParams(pasted);
|
|
124
|
+
const decodedState = decodeState(params.state);
|
|
125
|
+
if (decodedState.verifier !== verifier) throw new Error("OAuth state verifier mismatch");
|
|
118
126
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
132
|
+
Accept: "*/*",
|
|
133
|
+
"User-Agent": getAntigravityHeaders("gemini-cli")["User-Agent"],
|
|
134
|
+
},
|
|
135
|
+
body: new URLSearchParams({
|
|
136
|
+
client_id: oauthClient.clientId,
|
|
137
|
+
...(oauthClient.clientSecret ? { client_secret: oauthClient.clientSecret } : {}),
|
|
138
|
+
code: params.code,
|
|
139
|
+
grant_type: "authorization_code",
|
|
140
|
+
redirect_uri: REDIRECT_URI,
|
|
141
|
+
code_verifier: verifier,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
|
|
145
|
+
const tokenPayload = (await tokenResponse.json()) as { access_token: string; refresh_token?: string; expires_in: number };
|
|
146
|
+
if (!tokenPayload.refresh_token) throw new Error("Missing refresh token in Google response");
|
|
139
147
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
const [projectId, email] = await Promise.all([fetchProjectId(tokenPayload.access_token), fetchGoogleUserEmail(tokenPayload.access_token)]);
|
|
149
|
+
return {
|
|
150
|
+
refresh: joinRefresh(tokenPayload.refresh_token, projectId ?? DEFAULT_PROJECT_ID),
|
|
151
|
+
access: encodeApiKey(tokenPayload.access_token, projectId ?? DEFAULT_PROJECT_ID),
|
|
152
|
+
expires: start + tokenPayload.expires_in * 1000 - TOKEN_EXPIRY_SKEW_MS,
|
|
153
|
+
...(email ? { email } : {}),
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
notifyAntigravityLoginFailure(error);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
export async function addAntigravityAccount(
|
|
@@ -151,7 +163,7 @@ export async function addAntigravityAccount(
|
|
|
151
163
|
options: { authPath?: string; activate?: boolean; email?: string } = {},
|
|
152
164
|
): Promise<AntigravityAddAccountResult> {
|
|
153
165
|
const authPath = options.authPath ?? getPiAuthPath();
|
|
154
|
-
const credentials = (await loginAntigravity(callbacks)) as OAuthCredentials & PiAuthCredential;
|
|
166
|
+
const credentials = (await loginAntigravity(callbacks, { authPath })) as OAuthCredentials & PiAuthCredential;
|
|
155
167
|
const refreshDetails = splitRefresh(credentials.refresh);
|
|
156
168
|
const projectId = refreshDetails.projectId || refreshDetails.managedProjectId || DEFAULT_PROJECT_ID;
|
|
157
169
|
const account: OpencodeAntigravityAccount = {
|
|
@@ -159,6 +171,7 @@ export async function addAntigravityAccount(
|
|
|
159
171
|
refreshToken: refreshDetails.refreshToken,
|
|
160
172
|
projectId,
|
|
161
173
|
managedProjectId: refreshDetails.managedProjectId,
|
|
174
|
+
...getGoogleOAuthClientCredentials(credentials),
|
|
162
175
|
enabled: true,
|
|
163
176
|
};
|
|
164
177
|
|
|
@@ -207,8 +220,11 @@ export async function addAntigravityAccount(
|
|
|
207
220
|
};
|
|
208
221
|
}
|
|
209
222
|
|
|
210
|
-
async function refreshAccountToken(account: OpencodeAntigravityAccount): Promise<RefreshedAntigravityAccount> {
|
|
211
|
-
|
|
223
|
+
async function refreshAccountToken(account: OpencodeAntigravityAccount, oauthClient?: GoogleOAuthClientCredentials): Promise<RefreshedAntigravityAccount> {
|
|
224
|
+
const refreshToken = getAccountRefreshToken(account);
|
|
225
|
+
if (!refreshToken) throw new Error(`Missing refresh token for Antigravity account ${account.email ?? "<unknown>"}`);
|
|
226
|
+
const clientCredentials = getGoogleOAuthClientCredentials(account) ?? oauthClient;
|
|
227
|
+
assertGoogleOAuthCredentialsConfigured(clientCredentials);
|
|
212
228
|
const projectId = getAccountProjectId(account);
|
|
213
229
|
const start = Date.now();
|
|
214
230
|
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
@@ -219,10 +235,10 @@ async function refreshAccountToken(account: OpencodeAntigravityAccount): Promise
|
|
|
219
235
|
"User-Agent": getAntigravityHeaders("gemini-cli")["User-Agent"],
|
|
220
236
|
},
|
|
221
237
|
body: new URLSearchParams({
|
|
222
|
-
client_id:
|
|
223
|
-
client_secret:
|
|
238
|
+
client_id: clientCredentials.clientId,
|
|
239
|
+
...(clientCredentials.clientSecret ? { client_secret: clientCredentials.clientSecret } : {}),
|
|
224
240
|
grant_type: "refresh_token",
|
|
225
|
-
refresh_token:
|
|
241
|
+
refresh_token: refreshToken,
|
|
226
242
|
}),
|
|
227
243
|
});
|
|
228
244
|
if (!response.ok) throw new Error(`Token refresh failed for ${account.email ?? "Antigravity account"}: ${await response.text()}`);
|
|
@@ -231,7 +247,7 @@ async function refreshAccountToken(account: OpencodeAntigravityAccount): Promise
|
|
|
231
247
|
account,
|
|
232
248
|
projectId,
|
|
233
249
|
credentials: {
|
|
234
|
-
refresh: joinRefresh(payload.refresh_token ??
|
|
250
|
+
refresh: joinRefresh(payload.refresh_token ?? refreshToken, projectId, account.managedProjectId),
|
|
235
251
|
access: encodeApiKey(payload.access_token, projectId),
|
|
236
252
|
expires: start + payload.expires_in * 1000 - TOKEN_EXPIRY_SKEW_MS,
|
|
237
253
|
email: account.email,
|
|
@@ -241,6 +257,7 @@ async function refreshAccountToken(account: OpencodeAntigravityAccount): Promise
|
|
|
241
257
|
|
|
242
258
|
export async function refreshAntigravityToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
243
259
|
const credentialDetails = credentials as OAuthCredentials & PiAuthCredential;
|
|
260
|
+
const oauthClient = getGoogleOAuthClientCredentials(credentialDetails);
|
|
244
261
|
const storedAccounts = getStoredAccounts(credentialDetails);
|
|
245
262
|
const baseIndex = clampAccountIndex(credentialDetails.activeIndex, storedAccounts.length);
|
|
246
263
|
const rotationAccount = storedAccounts.length > 0 ? storedAccounts[(baseIndex + 1) % storedAccounts.length] : undefined;
|
|
@@ -253,6 +270,7 @@ export async function refreshAntigravityToken(credentials: OAuthCredentials): Pr
|
|
|
253
270
|
managedProjectId: fallback.managedProjectId,
|
|
254
271
|
email: credentialDetails.email,
|
|
255
272
|
},
|
|
273
|
+
oauthClient,
|
|
256
274
|
);
|
|
257
275
|
return {
|
|
258
276
|
...refreshed.credentials,
|
|
@@ -260,6 +278,55 @@ export async function refreshAntigravityToken(credentials: OAuthCredentials): Pr
|
|
|
260
278
|
};
|
|
261
279
|
}
|
|
262
280
|
|
|
281
|
+
export async function refreshStoredAntigravityCredential(authPath = getPiAuthPath()): Promise<AntigravityFailoverCredential | undefined> {
|
|
282
|
+
const auth = await readJsonFile<PiAuthData>(authPath, {});
|
|
283
|
+
const current = auth[PROVIDER_ID];
|
|
284
|
+
if (current?.type !== "oauth") return undefined;
|
|
285
|
+
|
|
286
|
+
const accounts = getStoredAccounts(current);
|
|
287
|
+
const oauthClient = getGoogleOAuthClientCredentials(current);
|
|
288
|
+
const activeIndex = accounts.length > 0 ? clampAccountIndex(current.activeIndex, accounts.length) : 0;
|
|
289
|
+
const fallback = splitRefresh(current.refresh ?? "");
|
|
290
|
+
const account = accounts[activeIndex] ?? {
|
|
291
|
+
refreshToken: fallback.refreshToken,
|
|
292
|
+
projectId: fallback.projectId || fallback.managedProjectId,
|
|
293
|
+
managedProjectId: fallback.managedProjectId,
|
|
294
|
+
email: current.email,
|
|
295
|
+
};
|
|
296
|
+
if (!account.refreshToken) return undefined;
|
|
297
|
+
|
|
298
|
+
const refreshed = await refreshAccountToken({ ...account, refreshToken: getAccountRefreshToken(account) }, oauthClient);
|
|
299
|
+
const refreshedParts = splitRefresh(refreshed.credentials.refresh);
|
|
300
|
+
const nextAccounts = accounts.length > 0
|
|
301
|
+
? accounts.map((stored, index) => index === activeIndex
|
|
302
|
+
? {
|
|
303
|
+
...stored,
|
|
304
|
+
refreshToken: refreshedParts.refreshToken || stored.refreshToken,
|
|
305
|
+
projectId: refreshed.projectId,
|
|
306
|
+
managedProjectId: account.managedProjectId,
|
|
307
|
+
email: account.email ?? stored.email,
|
|
308
|
+
enabled: stored.enabled !== false,
|
|
309
|
+
}
|
|
310
|
+
: stored)
|
|
311
|
+
: [];
|
|
312
|
+
const nextCredential: PiAuthCredential = {
|
|
313
|
+
...current,
|
|
314
|
+
type: "oauth",
|
|
315
|
+
...refreshed.credentials,
|
|
316
|
+
...(nextAccounts.length > 0 ? { accounts: nextAccounts, activeIndex, email: account.email ?? current.email } : {}),
|
|
317
|
+
};
|
|
318
|
+
auth[PROVIDER_ID] = nextCredential;
|
|
319
|
+
await writeJsonFileSecure(authPath, auth);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
apiKey: nextCredential.access ?? "",
|
|
323
|
+
projectId: refreshed.projectId,
|
|
324
|
+
email: account.email,
|
|
325
|
+
accountIndex: activeIndex,
|
|
326
|
+
accountCount: accounts.length || 1,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
263
330
|
export async function refreshNextFailoverCredential(attemptedAccountIndices: Set<number>): Promise<AntigravityFailoverCredential | undefined> {
|
|
264
331
|
const authPath = getPiAuthPath();
|
|
265
332
|
const auth = await readJsonFile<PiAuthData>(authPath, {});
|
|
@@ -267,6 +334,7 @@ export async function refreshNextFailoverCredential(attemptedAccountIndices: Set
|
|
|
267
334
|
if (current?.type !== "oauth") return undefined;
|
|
268
335
|
|
|
269
336
|
const accounts = getStoredAccounts(current);
|
|
337
|
+
const oauthClient = getGoogleOAuthClientCredentials(current);
|
|
270
338
|
if (accounts.length <= attemptedAccountIndices.size) return undefined;
|
|
271
339
|
const baseIndex = clampAccountIndex(current.activeIndex, accounts.length);
|
|
272
340
|
let lastRefreshError: unknown;
|
|
@@ -277,7 +345,7 @@ export async function refreshNextFailoverCredential(attemptedAccountIndices: Set
|
|
|
277
345
|
attemptedAccountIndices.add(accountIndex);
|
|
278
346
|
const account = accounts[accountIndex];
|
|
279
347
|
try {
|
|
280
|
-
const refreshed = await refreshAccountToken(account);
|
|
348
|
+
const refreshed = await refreshAccountToken(account, oauthClient);
|
|
281
349
|
const nextCredential: PiAuthCredential = {
|
|
282
350
|
...current,
|
|
283
351
|
type: "oauth",
|
|
@@ -7,6 +7,9 @@ import type { AntigravityStatusDetails, OpencodeAntigravityAccount, PiAuthData,
|
|
|
7
7
|
|
|
8
8
|
let extensionUi: ExtensionUIContext | undefined;
|
|
9
9
|
let extensionApi: ExtensionAPI | undefined;
|
|
10
|
+
const notifiedLoginFailures = new WeakSet<object>();
|
|
11
|
+
const PROVIDER_FAILURE_DEDUPE_MS = 60_000;
|
|
12
|
+
const notifiedProviderFailures = new Map<string, number>();
|
|
10
13
|
|
|
11
14
|
export function rememberAntigravityApi(api: ExtensionAPI): void {
|
|
12
15
|
extensionApi = api;
|
|
@@ -16,6 +19,80 @@ export function rememberAntigravityUi(ui: ExtensionUIContext | undefined): void
|
|
|
16
19
|
if (ui) extensionUi = ui;
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
function errorMessage(error: unknown): string {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatAntigravityLoginFailure(error: unknown): string {
|
|
27
|
+
return `Antigravity login failed: ${errorMessage(error)}. Auth file: ${getPiAuthPath()}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatAntigravityProviderFailure(error: unknown): string {
|
|
31
|
+
return `Antigravity request failed: ${errorMessage(error)}. Auth file: ${getPiAuthPath()}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function notifyAntigravityFailure(message: string, details: Record<string, unknown>, options: { ui?: ExtensionUIContext; sendSessionMessage?: boolean } = {}): void {
|
|
35
|
+
const { ui, sendSessionMessage = true } = options;
|
|
36
|
+
const targetUi = ui ?? extensionUi;
|
|
37
|
+
if (typeof targetUi?.notify === "function") {
|
|
38
|
+
targetUi.notify(message, "error");
|
|
39
|
+
} else if (typeof (targetUi as any)?.toast?.error === "function") {
|
|
40
|
+
(targetUi as any).toast.error(message);
|
|
41
|
+
}
|
|
42
|
+
if (!sendSessionMessage) return;
|
|
43
|
+
(extensionApi as any)?.sendMessage?.({
|
|
44
|
+
customType: "antigravity-auth-status",
|
|
45
|
+
content: message,
|
|
46
|
+
display: true,
|
|
47
|
+
details,
|
|
48
|
+
}, { triggerTurn: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldNotifyProviderFailure(message: string, model?: string): boolean {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const key = `${model ?? ""}\0${message}`;
|
|
54
|
+
for (const [existingKey, timestamp] of notifiedProviderFailures) {
|
|
55
|
+
if (now - timestamp > PROVIDER_FAILURE_DEDUPE_MS) notifiedProviderFailures.delete(existingKey);
|
|
56
|
+
}
|
|
57
|
+
const previous = notifiedProviderFailures.get(key);
|
|
58
|
+
if (previous !== undefined && now - previous <= PROVIDER_FAILURE_DEDUPE_MS) return false;
|
|
59
|
+
notifiedProviderFailures.set(key, now);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function notifyAntigravityLoginFailure(error: unknown): boolean {
|
|
64
|
+
if (typeof error === "object" && error !== null) {
|
|
65
|
+
if (notifiedLoginFailures.has(error)) return false;
|
|
66
|
+
notifiedLoginFailures.add(error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const message = formatAntigravityLoginFailure(error);
|
|
70
|
+
notifyAntigravityFailure(message, {
|
|
71
|
+
kind: "login-failure",
|
|
72
|
+
authPath: getPiAuthPath(),
|
|
73
|
+
error: errorMessage(error),
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function notifyAntigravityProviderFailure(error: unknown, options: { ui?: ExtensionUIContext; model?: string } = {}): boolean {
|
|
79
|
+
const message = formatAntigravityProviderFailure(error);
|
|
80
|
+
if (!shouldNotifyProviderFailure(message, options.model)) return false;
|
|
81
|
+
notifyAntigravityFailure(message, {
|
|
82
|
+
kind: "provider-failure",
|
|
83
|
+
authPath: getPiAuthPath(),
|
|
84
|
+
error: errorMessage(error),
|
|
85
|
+
model: options.model,
|
|
86
|
+
}, {
|
|
87
|
+
ui: options.ui,
|
|
88
|
+
// Provider failures are raised while the agent is still streaming. pi.sendMessage()
|
|
89
|
+
// would be delivered as a steering message in that state, which can trigger
|
|
90
|
+
// another Antigravity request and create an endless toast/request loop.
|
|
91
|
+
sendSessionMessage: false,
|
|
92
|
+
});
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
19
96
|
export async function getCurrentAntigravityStatus(): Promise<AntigravityStatusDetails> {
|
|
20
97
|
const auth = await readJsonFile<PiAuthData>(getPiAuthPath(), {});
|
|
21
98
|
const credential = auth[PROVIDER_ID];
|
|
@@ -51,7 +128,7 @@ async function startupAntigravityAccountList(): Promise<string> {
|
|
|
51
128
|
try {
|
|
52
129
|
const auth = await readJsonFile<PiAuthData>(getPiAuthPath(), {});
|
|
53
130
|
const accounts = getStartupAccounts(auth[PROVIDER_ID]);
|
|
54
|
-
if (accounts.length === 0) return "no accounts (run /antigravity-
|
|
131
|
+
if (accounts.length === 0) return "no accounts in auth.json (run /antigravity-add-account)";
|
|
55
132
|
return accounts.map(formatStartupAccountName).join(", ");
|
|
56
133
|
} catch (error) {
|
|
57
134
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -68,8 +145,10 @@ export async function publishAntigravityAuthStartupSection(): Promise<void> {
|
|
|
68
145
|
}
|
|
69
146
|
|
|
70
147
|
export function emitAntigravityStatus(details: AntigravityStatusDetails): void {
|
|
71
|
-
extensionUi?.setStatus
|
|
72
|
-
|
|
148
|
+
if (typeof extensionUi?.setStatus === "function") {
|
|
149
|
+
extensionUi.setStatus(LEGACY_STATUS_KEY, undefined);
|
|
150
|
+
extensionUi.setStatus(STATUS_KEY, formatAntigravityStatus(details));
|
|
151
|
+
}
|
|
73
152
|
(extensionApi as any)?.sendMessage?.({
|
|
74
153
|
role: "system",
|
|
75
154
|
content: formatAntigravityStatus(details),
|
|
@@ -3,7 +3,7 @@ import { calculateCost, createAssistantMessageEventStream, type AssistantMessage
|
|
|
3
3
|
import { ALL_ACCOUNTS_EXHAUSTED_MARKER, API_ID, ENDPOINT_PROD, PROVIDER_ID, STREAM_ENDPOINTS } from "./constants";
|
|
4
4
|
import { clampAccountIndex, decodeApiKey, getPiAuthPath, getStoredAccounts, readJsonFile } from "./auth-store";
|
|
5
5
|
import { getAntigravityHeaders, getModelHeaderStyle } from "./headers";
|
|
6
|
-
import { refreshNextFailoverCredential } from "./oauth";
|
|
6
|
+
import { refreshNextFailoverCredential, refreshStoredAntigravityCredential } from "./oauth";
|
|
7
7
|
import { buildPayload, extraHeadersForPayload, partThoughtSignature } from "./payload";
|
|
8
8
|
import { emitAntigravityStatus } from "./status";
|
|
9
9
|
import type { AntigravityChunk, AntigravityModel, PiAuthData } from "./types";
|
|
@@ -104,6 +104,24 @@ function updateUsage(output: AssistantMessage, model: AntigravityModel, metadata
|
|
|
104
104
|
calculateCost(model, output.usage);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
async function resolveAntigravityApiKey(optionsApiKey?: string): Promise<{ auth: PiAuthData; apiKey: string }> {
|
|
108
|
+
const auth = await readJsonFile<PiAuthData>(getPiAuthPath(), {});
|
|
109
|
+
const storedCredential = auth[PROVIDER_ID];
|
|
110
|
+
|
|
111
|
+
const storedApiKey = storedCredential?.type === "oauth" && storedCredential.access && (storedCredential.expires ?? 0) > Date.now()
|
|
112
|
+
? storedCredential.access
|
|
113
|
+
: undefined;
|
|
114
|
+
const apiKey = storedApiKey ?? optionsApiKey;
|
|
115
|
+
if (apiKey) return { auth, apiKey };
|
|
116
|
+
|
|
117
|
+
if (storedCredential?.type === "oauth") {
|
|
118
|
+
const refreshed = await refreshStoredAntigravityCredential();
|
|
119
|
+
if (refreshed?.apiKey) return { auth: await readJsonFile<PiAuthData>(getPiAuthPath(), {}), apiKey: refreshed.apiKey };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(`No Antigravity OAuth account found in Pi auth: ${getPiAuthPath()}.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
107
125
|
export function streamAntigravity(model: AntigravityModel, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
108
126
|
const stream = createAssistantMessageEventStream();
|
|
109
127
|
(async () => {
|
|
@@ -135,12 +153,7 @@ export function streamAntigravity(model: AntigravityModel, context: Context, opt
|
|
|
135
153
|
|
|
136
154
|
try {
|
|
137
155
|
const attemptedAccountIndices = new Set<number>();
|
|
138
|
-
const auth = await
|
|
139
|
-
const storedCredential = auth[PROVIDER_ID];
|
|
140
|
-
const apiKey = storedCredential?.type === "oauth" && storedCredential.access && (storedCredential.expires ?? 0) > Date.now()
|
|
141
|
-
? storedCredential.access
|
|
142
|
-
: options?.apiKey;
|
|
143
|
-
if (!apiKey) throw new Error("Not authenticated with Antigravity. Run /login antigravity first.");
|
|
156
|
+
const { auth, apiKey } = await resolveAntigravityApiKey(options?.apiKey);
|
|
144
157
|
const decodedApiKey = decodeApiKey(apiKey);
|
|
145
158
|
const authAccounts = getStoredAccounts(auth[PROVIDER_ID]);
|
|
146
159
|
if (authAccounts.length > 0) attemptedAccountIndices.add(clampAccountIndex(auth[PROVIDER_ID]?.activeIndex, authAccounts.length));
|
|
@@ -39,10 +39,26 @@ export type AntigravityChunk = {
|
|
|
39
39
|
|
|
40
40
|
export type OpencodeAntigravityAccount = {
|
|
41
41
|
email?: string;
|
|
42
|
+
access?: string;
|
|
43
|
+
refresh?: string;
|
|
44
|
+
expires?: number;
|
|
42
45
|
refreshToken?: string;
|
|
43
46
|
projectId?: string;
|
|
44
47
|
managedProjectId?: string;
|
|
45
48
|
enabled?: boolean;
|
|
49
|
+
clientId?: string;
|
|
50
|
+
clientSecret?: string;
|
|
51
|
+
googleClientId?: string;
|
|
52
|
+
googleClientSecret?: string;
|
|
53
|
+
oauthClient?: GoogleOAuthClientCredentials;
|
|
54
|
+
fingerprint?: { apiClient?: string; [key: string]: unknown };
|
|
55
|
+
fingerprintHistory?: Array<{ fingerprint?: { apiClient?: string; [key: string]: unknown }; [key: string]: unknown }>;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type GoogleOAuthClientCredentials = {
|
|
60
|
+
clientId?: string;
|
|
61
|
+
clientSecret?: string;
|
|
46
62
|
};
|
|
47
63
|
|
|
48
64
|
export type OpencodeAntigravityStorage = {
|
|
@@ -59,6 +75,11 @@ export type PiAuthCredential = {
|
|
|
59
75
|
email?: string;
|
|
60
76
|
accounts?: OpencodeAntigravityAccount[];
|
|
61
77
|
activeIndex?: number;
|
|
78
|
+
clientId?: string;
|
|
79
|
+
clientSecret?: string;
|
|
80
|
+
googleClientId?: string;
|
|
81
|
+
googleClientSecret?: string;
|
|
82
|
+
oauthClient?: GoogleOAuthClientCredentials;
|
|
62
83
|
[key: string]: unknown;
|
|
63
84
|
};
|
|
64
85
|
|
|
@@ -8,11 +8,13 @@ import { DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC } from "./default-pi-tools-suite-co
|
|
|
8
8
|
export interface PiToolsSuiteConfig {
|
|
9
9
|
enabled: boolean;
|
|
10
10
|
disabledModules: string[];
|
|
11
|
+
todoThinking: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
type MutableConfig = {
|
|
14
15
|
enabled: boolean;
|
|
15
16
|
disabledModules: Set<string>;
|
|
17
|
+
todoThinking: boolean;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
type Env = Record<string, string | undefined>;
|
|
@@ -107,6 +109,7 @@ function removeDisabled(config: MutableConfig, value: unknown, knownModules: Rea
|
|
|
107
109
|
|
|
108
110
|
function mergeConfigLayer(config: MutableConfig, raw: Record<string, unknown>, knownModules: ReadonlySet<string>): MutableConfig {
|
|
109
111
|
if (typeof raw.enabled === "boolean") config.enabled = raw.enabled;
|
|
112
|
+
if (typeof raw.todoThinking === "boolean") config.todoThinking = raw.todoThinking;
|
|
110
113
|
|
|
111
114
|
for (const key of DISABLED_LIST_KEYS) addDisabled(config, raw[key], knownModules);
|
|
112
115
|
for (const key of ENABLED_LIST_KEYS) removeDisabled(config, raw[key], knownModules);
|
|
@@ -150,6 +153,9 @@ function applyEnv(config: MutableConfig, env: Env, knownModules: ReadonlySet<str
|
|
|
150
153
|
addDisabled(config, env.PI_TOOLS_SUITE_DISABLED_MODULES, knownModules);
|
|
151
154
|
addDisabled(config, env.PI_TOOLS_SUITE_DISABLED_EXTENSIONS, knownModules);
|
|
152
155
|
|
|
156
|
+
const todoThinking = boolFromEnv(env.PI_TOOLS_SUITE_TODO_THINKING);
|
|
157
|
+
if (todoThinking !== undefined) config.todoThinking = todoThinking;
|
|
158
|
+
|
|
153
159
|
return config;
|
|
154
160
|
}
|
|
155
161
|
|
|
@@ -159,6 +165,7 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
|
|
|
159
165
|
const config: MutableConfig = {
|
|
160
166
|
enabled: true,
|
|
161
167
|
disabledModules: new Set([...DEFAULT_DISABLED_MODULES].filter((name) => knownModules.has(name))),
|
|
168
|
+
todoThinking: false,
|
|
162
169
|
};
|
|
163
170
|
const userConfigPath = getPiToolsSuiteUserConfigPath(options.homeDir);
|
|
164
171
|
|
|
@@ -176,5 +183,6 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
|
|
|
176
183
|
return {
|
|
177
184
|
enabled: config.enabled,
|
|
178
185
|
disabledModules: [...config.disabledModules].sort(),
|
|
186
|
+
todoThinking: config.todoThinking,
|
|
179
187
|
};
|
|
180
188
|
}
|
|
@@ -105,7 +105,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
105
105
|
// ── 2. Create state ───────────────────────────────────────────────────────
|
|
106
106
|
const state = createState()
|
|
107
107
|
const appendNudgeTelemetry = (
|
|
108
|
-
event: "emitted" | "upgraded",
|
|
108
|
+
event: "emitted" | "upgraded" | "reapplied",
|
|
109
109
|
type: DcpNudgeType,
|
|
110
110
|
anchor: { id: number; anchorTimestamp: number; anchorStableId?: string; anchorRole: string },
|
|
111
111
|
usage: ReturnType<typeof normalizeDcpContextUsage>,
|
|
@@ -329,6 +329,21 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
329
329
|
toolCallsSinceLastUser,
|
|
330
330
|
)
|
|
331
331
|
saveState(pi, state)
|
|
332
|
+
} else {
|
|
333
|
+
// Anchor already exists at >= priority; the reminder text is
|
|
334
|
+
// re-applied below via applyAnchoredNudges on every context
|
|
335
|
+
// event. Emit 'reapplied' so telemetry reflects every active
|
|
336
|
+
// reminder delivery, not just creates/upgrades. Without this
|
|
337
|
+
// branch the user/developer sees a single "emitted" entry even
|
|
338
|
+
// when the LLM was reminded many times across a long autonomous
|
|
339
|
+
// loop, which made auto-nudge look silent when it actually ran.
|
|
340
|
+
appendNudgeTelemetry(
|
|
341
|
+
"reapplied",
|
|
342
|
+
anchorResult.anchor.type,
|
|
343
|
+
anchorResult.anchor,
|
|
344
|
+
usage,
|
|
345
|
+
toolCallsSinceLastUser,
|
|
346
|
+
)
|
|
332
347
|
}
|
|
333
348
|
} else {
|
|
334
349
|
// No safe existing message could be anchored (rare); keep the older
|
|
@@ -283,6 +283,20 @@ export interface SerializedDcpState {
|
|
|
283
283
|
nudgeAnchors?: DcpNudgeAnchor[]
|
|
284
284
|
nextNudgeAnchorId?: number
|
|
285
285
|
lastNudge?: DcpLastNudge
|
|
286
|
+
/**
|
|
287
|
+
* Persisted since v??. `context` events re-seed currentTurn from raw
|
|
288
|
+
* messages, but keeping it across session restarts gives diagnostics and
|
|
289
|
+
* telemetry a contiguous turn counter instead of resetting to 0.
|
|
290
|
+
*/
|
|
291
|
+
currentTurn?: number
|
|
292
|
+
/**
|
|
293
|
+
* Persisted since v?.?. Without persistence a pi process restart silently
|
|
294
|
+
* reset the nudge cadence counter to 0, which could suppress the next
|
|
295
|
+
* reminder on a session that was already near a context threshold.
|
|
296
|
+
*/
|
|
297
|
+
nudgeCounter?: number
|
|
298
|
+
/** Persisted since v?.?. Diagnostic turn of the last emitted nudge. */
|
|
299
|
+
lastNudgeTurn?: number
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
function isToolRecord(value: unknown): value is ToolRecord {
|
|
@@ -337,6 +351,9 @@ export function serializeState(state: DcpState): SerializedDcpState {
|
|
|
337
351
|
nudgeAnchors: state.nudgeAnchors,
|
|
338
352
|
nextNudgeAnchorId: state.nextNudgeAnchorId,
|
|
339
353
|
lastNudge: state.lastNudge,
|
|
354
|
+
currentTurn: state.currentTurn,
|
|
355
|
+
nudgeCounter: state.nudgeCounter,
|
|
356
|
+
lastNudgeTurn: state.lastNudgeTurn,
|
|
340
357
|
}
|
|
341
358
|
}
|
|
342
359
|
|
|
@@ -444,6 +461,24 @@ export function restoreState(state: DcpState, data: unknown): void {
|
|
|
444
461
|
if (isLastNudge(saved.lastNudge)) {
|
|
445
462
|
state.lastNudge = saved.lastNudge
|
|
446
463
|
}
|
|
464
|
+
|
|
465
|
+
// nudgeCounter: clamp to a non-negative integer so a corrupted payload
|
|
466
|
+
// cannot stall reminders by going negative. Default 0 keeps older sessions
|
|
467
|
+
// behaving like a fresh cadence on first post-restart context event.
|
|
468
|
+
if (typeof saved.nudgeCounter === "number" && Number.isFinite(saved.nudgeCounter) && saved.nudgeCounter >= 0) {
|
|
469
|
+
state.nudgeCounter = Math.floor(saved.nudgeCounter)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// lastNudgeTurn and currentTurn default to createState() values when absent
|
|
473
|
+
// (lastNudgeTurn = -1, currentTurn = 0). currentTurn is re-derived from
|
|
474
|
+
// raw messages on every `context` event, so persistence here is best-effort
|
|
475
|
+
// for telemetry continuity rather than authoritative.
|
|
476
|
+
if (typeof saved.lastNudgeTurn === "number" && Number.isFinite(saved.lastNudgeTurn)) {
|
|
477
|
+
state.lastNudgeTurn = Math.floor(saved.lastNudgeTurn)
|
|
478
|
+
}
|
|
479
|
+
if (typeof saved.currentTurn === "number" && Number.isFinite(saved.currentTurn) && saved.currentTurn >= 0) {
|
|
480
|
+
state.currentTurn = Math.floor(saved.currentTurn)
|
|
481
|
+
}
|
|
447
482
|
}
|
|
448
483
|
|
|
449
484
|
// ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,9 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
5
5
|
// "ast-grep",
|
|
6
6
|
// "dcp"
|
|
7
7
|
],
|
|
8
|
+
// When true, todo items may carry a per-task thinking level and the todo
|
|
9
|
+
// module will switch/restore Pi's thinking level as in-progress tasks change.
|
|
10
|
+
"todoThinking": false,
|
|
8
11
|
"terminalBell": { "sound": true },
|
|
9
12
|
"dcp": {
|
|
10
13
|
"enabled": true,
|