pi-ui-extend 0.1.13 → 0.1.15
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 +5 -0
- package/dist/app/app.js +82 -12
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +3 -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.js +3 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +8 -2
- 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 +2 -0
- package/dist/app/popup/menu-items-controller.js +45 -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 +68 -322
- 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 +4 -2
- package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
- package/dist/app/rendering/popup-menu-renderer.js +307 -0
- package/dist/app/rendering/render-controller.js +5 -13
- package/dist/app/rendering/status-line-renderer.d.ts +1 -1
- package/dist/app/rendering/status-line-renderer.js +27 -24
- package/dist/app/rendering/toast-controller.d.ts +11 -3
- package/dist/app/rendering/toast-controller.js +53 -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 +11 -9
- package/dist/app/screen/scroll-controller.js +50 -45
- 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/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/tabs-controller.d.ts +13 -1
- package/dist/app/session/tabs-controller.js +248 -27
- package/dist/app/todo/todo-model.d.ts +3 -1
- package/dist/app/todo/todo-model.js +14 -2
- package/dist/app/types.d.ts +5 -2
- 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 +5 -1
- package/dist/config.js +73 -25
- package/dist/default-pix-config.js +2 -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 +2 -1
- package/dist/schemas/pix-schema.js +5 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/external/pi-tools-suite/README.md +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
- 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 +181 -11
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
- package/external/pi-tools-suite/src/todo/todo.ts +10 -5
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
- package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +6 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
|
3
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,
|
|
4
|
+
import { accountFromCredential, clampAccountIndex, encodeApiKey, findMatchingAccountIndex, getAccountProjectId, getPiAuthPath, getStoredAccounts, joinRefresh, readJsonFile, splitRefresh, writeJsonFileSecure } from "./auth-store";
|
|
5
5
|
import { getAntigravityHeaders } from "./headers";
|
|
6
|
+
import { notifyAntigravityLoginFailure } from "./status";
|
|
6
7
|
import type { AntigravityAddAccountResult, AntigravityFailoverCredential, AntigravityLoginCallbacks, OpencodeAntigravityAccount, PiAuthCredential, PiAuthData, RefreshedAntigravityAccount } from "./types";
|
|
7
8
|
|
|
8
9
|
function base64Url(input: Buffer): string {
|
|
@@ -91,59 +92,62 @@ function extractOAuthParams(input: string): { code: string; state: string } {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export async function loginAntigravity(callbacks: AntigravityLoginCallbacks): Promise<OAuthCredentials> {
|
|
94
|
-
|
|
95
|
-
throw new Error("Antigravity Google OAuth credentials are not
|
|
96
|
-
}
|
|
95
|
+
try {
|
|
96
|
+
if (!CLIENT_ID || !CLIENT_SECRET) throw new Error("Antigravity Google OAuth credentials are not bundled.");
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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");
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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");
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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");
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
notifyAntigravityLoginFailure(error);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
export async function addAntigravityAccount(
|
|
@@ -260,6 +264,54 @@ export async function refreshAntigravityToken(credentials: OAuthCredentials): Pr
|
|
|
260
264
|
};
|
|
261
265
|
}
|
|
262
266
|
|
|
267
|
+
export async function refreshStoredAntigravityCredential(authPath = getPiAuthPath()): Promise<AntigravityFailoverCredential | undefined> {
|
|
268
|
+
const auth = await readJsonFile<PiAuthData>(authPath, {});
|
|
269
|
+
const current = auth[PROVIDER_ID];
|
|
270
|
+
if (current?.type !== "oauth") return undefined;
|
|
271
|
+
|
|
272
|
+
const accounts = getStoredAccounts(current);
|
|
273
|
+
const activeIndex = accounts.length > 0 ? clampAccountIndex(current.activeIndex, accounts.length) : 0;
|
|
274
|
+
const fallback = splitRefresh(current.refresh ?? "");
|
|
275
|
+
const account = accounts[activeIndex] ?? {
|
|
276
|
+
refreshToken: fallback.refreshToken,
|
|
277
|
+
projectId: fallback.projectId || fallback.managedProjectId,
|
|
278
|
+
managedProjectId: fallback.managedProjectId,
|
|
279
|
+
email: current.email,
|
|
280
|
+
};
|
|
281
|
+
if (!account.refreshToken) return undefined;
|
|
282
|
+
|
|
283
|
+
const refreshed = await refreshAccountToken({ ...account, refreshToken: account.refreshToken });
|
|
284
|
+
const refreshedParts = splitRefresh(refreshed.credentials.refresh);
|
|
285
|
+
const nextAccounts = accounts.length > 0
|
|
286
|
+
? accounts.map((stored, index) => index === activeIndex
|
|
287
|
+
? {
|
|
288
|
+
...stored,
|
|
289
|
+
refreshToken: refreshedParts.refreshToken || stored.refreshToken,
|
|
290
|
+
projectId: refreshed.projectId,
|
|
291
|
+
managedProjectId: account.managedProjectId,
|
|
292
|
+
email: account.email ?? stored.email,
|
|
293
|
+
enabled: stored.enabled !== false,
|
|
294
|
+
}
|
|
295
|
+
: stored)
|
|
296
|
+
: [];
|
|
297
|
+
const nextCredential: PiAuthCredential = {
|
|
298
|
+
...current,
|
|
299
|
+
type: "oauth",
|
|
300
|
+
...refreshed.credentials,
|
|
301
|
+
...(nextAccounts.length > 0 ? { accounts: nextAccounts, activeIndex, email: account.email ?? current.email } : {}),
|
|
302
|
+
};
|
|
303
|
+
auth[PROVIDER_ID] = nextCredential;
|
|
304
|
+
await writeJsonFileSecure(authPath, auth);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
apiKey: nextCredential.access ?? "",
|
|
308
|
+
projectId: refreshed.projectId,
|
|
309
|
+
email: account.email,
|
|
310
|
+
accountIndex: activeIndex,
|
|
311
|
+
accountCount: accounts.length || 1,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
263
315
|
export async function refreshNextFailoverCredential(attemptedAccountIndices: Set<number>): Promise<AntigravityFailoverCredential | undefined> {
|
|
264
316
|
const authPath = getPiAuthPath();
|
|
265
317
|
const auth = await readJsonFile<PiAuthData>(authPath, {});
|
|
@@ -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];
|
|
@@ -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),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { calculateCost, createAssistantMessageEventStream, type AssistantMessage, type AssistantMessageEventStream, type Context, type SimpleStreamOptions, type ToolCall } from "@earendil-works/pi-ai";
|
|
3
3
|
import { ALL_ACCOUNTS_EXHAUSTED_MARKER, API_ID, ENDPOINT_PROD, PROVIDER_ID, STREAM_ENDPOINTS } from "./constants";
|
|
4
|
-
import { clampAccountIndex, decodeApiKey, getPiAuthPath, getStoredAccounts, readJsonFile } from "./auth-store";
|
|
4
|
+
import { clampAccountIndex, decodeApiKey, getDefaultOpencodeAccountsPath, getPiAuthPath, getStoredAccounts, importDefaultOpencodeAntigravityAccount, 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,32 @@ 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
|
+
let auth = await readJsonFile<PiAuthData>(getPiAuthPath(), {});
|
|
109
|
+
let storedCredential = auth[PROVIDER_ID];
|
|
110
|
+
|
|
111
|
+
if (storedCredential?.type !== "oauth") {
|
|
112
|
+
const imported = await importDefaultOpencodeAntigravityAccount().catch(() => undefined);
|
|
113
|
+
if (imported?.imported || imported?.reason === "already-imported") {
|
|
114
|
+
auth = await readJsonFile<PiAuthData>(getPiAuthPath(), {});
|
|
115
|
+
storedCredential = auth[PROVIDER_ID];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const storedApiKey = storedCredential?.type === "oauth" && storedCredential.access && (storedCredential.expires ?? 0) > Date.now()
|
|
120
|
+
? storedCredential.access
|
|
121
|
+
: undefined;
|
|
122
|
+
const apiKey = storedApiKey ?? optionsApiKey;
|
|
123
|
+
if (apiKey) return { auth, apiKey };
|
|
124
|
+
|
|
125
|
+
if (storedCredential?.type === "oauth") {
|
|
126
|
+
const refreshed = await refreshStoredAntigravityCredential();
|
|
127
|
+
if (refreshed?.apiKey) return { auth: await readJsonFile<PiAuthData>(getPiAuthPath(), {}), apiKey: refreshed.apiKey };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(`No Antigravity OAuth account found. Checked Pi auth: ${getPiAuthPath()}; opencode accounts: ${getDefaultOpencodeAccountsPath()}.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
export function streamAntigravity(model: AntigravityModel, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
108
134
|
const stream = createAssistantMessageEventStream();
|
|
109
135
|
(async () => {
|
|
@@ -135,12 +161,7 @@ export function streamAntigravity(model: AntigravityModel, context: Context, opt
|
|
|
135
161
|
|
|
136
162
|
try {
|
|
137
163
|
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.");
|
|
164
|
+
const { auth, apiKey } = await resolveAntigravityApiKey(options?.apiKey);
|
|
144
165
|
const decodedApiKey = decodeApiKey(apiKey);
|
|
145
166
|
const authAccounts = getStoredAccounts(auth[PROVIDER_ID]);
|
|
146
167
|
if (authAccounts.length > 0) attemptedAccountIndices.add(clampAccountIndex(auth[PROVIDER_ID]?.activeIndex, authAccounts.length));
|
|
@@ -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,
|