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.
Files changed (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. 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 { CLIENT_ID, CLIENT_SECRET, DEFAULT_PROJECT_ID, LOAD_ENDPOINTS, REDIRECT_URI, SCOPES, TOKEN_EXPIRY_SKEW_MS, PROVIDER_ID } from "./constants";
4
- import { accountFromCredential, clampAccountIndex, decodeApiKey, encodeApiKey, findMatchingAccountIndex, getAccountProjectId, getPiAuthPath, getStoredAccounts, joinRefresh, readJsonFile, splitRefresh, writeJsonFileSecure } from "./auth-store";
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 type { AntigravityAddAccountResult, AntigravityFailoverCredential, AntigravityLoginCallbacks, OpencodeAntigravityAccount, PiAuthCredential, PiAuthData, RefreshedAntigravityAccount } from "./types";
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
- if (!CLIENT_ID || !CLIENT_SECRET) {
95
- throw new Error("Antigravity Google OAuth credentials are not configured; set PIX_ANTIGRAVITY_GOOGLE_CLIENT_ID and PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET.");
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
- const { verifier, challenge } = generatePkce();
99
- const state = encodeState({ verifier });
100
- const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
101
- url.searchParams.set("client_id", CLIENT_ID);
102
- url.searchParams.set("response_type", "code");
103
- url.searchParams.set("redirect_uri", REDIRECT_URI);
104
- url.searchParams.set("scope", SCOPES.join(" "));
105
- url.searchParams.set("code_challenge", challenge);
106
- url.searchParams.set("code_challenge_method", "S256");
107
- url.searchParams.set("state", state);
108
- url.searchParams.set("access_type", "offline");
109
- url.searchParams.set("prompt", "consent");
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
- callbacks.onAuth({ url: url.toString() });
112
- const pasted = await callbacks.onPrompt({
113
- message: "Paste the full http://localhost:51121/oauth-callback URL after Google login (or code#state):",
114
- });
115
- const params = extractOAuthParams(pasted);
116
- const decodedState = decodeState(params.state);
117
- if (decodedState.verifier !== verifier) throw new Error("OAuth state verifier mismatch");
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
- const start = Date.now();
120
- const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
121
- method: "POST",
122
- headers: {
123
- "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
124
- Accept: "*/*",
125
- "User-Agent": getAntigravityHeaders("gemini-cli")["User-Agent"],
126
- },
127
- body: new URLSearchParams({
128
- client_id: CLIENT_ID,
129
- client_secret: CLIENT_SECRET,
130
- code: params.code,
131
- grant_type: "authorization_code",
132
- redirect_uri: REDIRECT_URI,
133
- code_verifier: verifier,
134
- }),
135
- });
136
- if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
137
- const tokenPayload = (await tokenResponse.json()) as { access_token: string; refresh_token?: string; expires_in: number };
138
- if (!tokenPayload.refresh_token) throw new Error("Missing refresh token in Google response");
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
- const [projectId, email] = await Promise.all([fetchProjectId(tokenPayload.access_token), fetchGoogleUserEmail(tokenPayload.access_token)]);
141
- return {
142
- refresh: joinRefresh(tokenPayload.refresh_token, projectId ?? DEFAULT_PROJECT_ID),
143
- access: encodeApiKey(tokenPayload.access_token, projectId ?? DEFAULT_PROJECT_ID),
144
- expires: start + tokenPayload.expires_in * 1000 - TOKEN_EXPIRY_SKEW_MS,
145
- ...(email ? { email } : {}),
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
- if (!account.refreshToken) throw new Error(`Missing refresh token for Antigravity account ${account.email ?? "<unknown>"}`);
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: CLIENT_ID,
223
- client_secret: CLIENT_SECRET,
238
+ client_id: clientCredentials.clientId,
239
+ ...(clientCredentials.clientSecret ? { client_secret: clientCredentials.clientSecret } : {}),
224
240
  grant_type: "refresh_token",
225
- refresh_token: account.refreshToken,
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 ?? account.refreshToken, projectId, account.managedProjectId),
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-import or /antigravity-add-account)";
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(LEGACY_STATUS_KEY, undefined);
72
- extensionUi?.setStatus(STATUS_KEY, formatAntigravityStatus(details));
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 readJsonFile<PiAuthData>(getPiAuthPath(), {});
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,