pi-ui-extend 0.1.29 → 0.1.31
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/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +31 -8
- package/dist/app/cli/update.d.ts +5 -0
- package/dist/app/cli/update.js +29 -1
- package/dist/app/model/model-usage-status.d.ts +2 -0
- package/dist/app/model/model-usage-status.js +90 -20
- package/dist/app/session/session-event-controller.d.ts +17 -1
- package/dist/app/session/session-event-controller.js +28 -0
- package/dist/app/session/tabs-controller.d.ts +10 -0
- package/dist/app/session/tabs-controller.js +65 -28
- package/external/pi-tools-suite/package.json +0 -3
- package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/index.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/render.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/tools/cleanup.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/tools/result.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +3 -3
- package/external/pi-tools-suite/src/async-subagents/tools/status.ts +3 -3
- package/external/pi-tools-suite/src/async-subagents/tools/stop.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -3
- package/external/pi-tools-suite/src/async-subagents/tools/wait.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/ui.ts +1 -1
- package/external/pi-tools-suite/src/dcp/commands.ts +2 -2
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +1 -1
- package/external/pi-tools-suite/src/dcp/index.ts +1 -1
- package/external/pi-tools-suite/src/model-tools/apply-patch.ts +1 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +2 -2
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +39 -4
- package/package.json +4 -7
package/dist/app/app.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export declare class PiUiExtendApp {
|
|
|
81
81
|
private setVoicePartialTranscript;
|
|
82
82
|
private addVoiceSystemMessage;
|
|
83
83
|
private resetSessionView;
|
|
84
|
+
private captureSessionView;
|
|
85
|
+
private restoreSessionView;
|
|
84
86
|
private loadSessionHistory;
|
|
85
87
|
private openSearchResultInNewTab;
|
|
86
88
|
private scrollToUserMessageJumpTarget;
|
package/dist/app/app.js
CHANGED
|
@@ -41,7 +41,7 @@ import { TabLineRenderer } from "./rendering/tab-line-renderer.js";
|
|
|
41
41
|
import { AppTerminalController } from "./terminal/terminal-controller.js";
|
|
42
42
|
import { TerminalBellSoundController } from "./terminal/terminal-bell-sound-controller.js";
|
|
43
43
|
import { AppToastController } from "./rendering/toast-controller.js";
|
|
44
|
-
import { checkPixUpdate, formatPixStartupUpdateDialog } from "./cli/update.js";
|
|
44
|
+
import { checkPiUpdate, checkPixUpdate, formatPixStartupUpdateDialog, formatPiStartupUpdateToast } from "./cli/update.js";
|
|
45
45
|
import { AppVoiceController } from "./input/voice-controller.js";
|
|
46
46
|
import { createIsolatedExtensionEventBus } from "./extensions/extension-event-bus.js";
|
|
47
47
|
import { setAppIconTheme } from "./icons.js";
|
|
@@ -181,6 +181,8 @@ export class PiUiExtendApp {
|
|
|
181
181
|
resetSessionView: () => this.resetSessionView(),
|
|
182
182
|
loadSessionHistory: () => this.loadSessionHistory(),
|
|
183
183
|
loadSessionHistoryAsync: (options) => this.loadSessionHistoryAsync(options),
|
|
184
|
+
captureSessionView: () => this.captureSessionView(),
|
|
185
|
+
restoreSessionView: (view) => this.restoreSessionView(view),
|
|
184
186
|
syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
|
|
185
187
|
captureInputState: () => this.inputEditor.draftState,
|
|
186
188
|
restoreInputState: (state) => this.restoreTabInputState(state),
|
|
@@ -785,18 +787,27 @@ export class PiUiExtendApp {
|
|
|
785
787
|
await this.sessionLifecycle.start();
|
|
786
788
|
this.modelUsageController.startPolling();
|
|
787
789
|
this.nerdFontController.ensureInstalledOnStartup();
|
|
788
|
-
|
|
790
|
+
this.checkPixUpdateOnStartup();
|
|
789
791
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
792
|
+
checkPixUpdateOnStartup() {
|
|
793
|
+
void checkPiUpdate()
|
|
794
|
+
.then((result) => {
|
|
795
|
+
if (result.status !== "newer")
|
|
796
|
+
return;
|
|
797
|
+
this.showToast(formatPiStartupUpdateToast(result), "warning");
|
|
798
|
+
})
|
|
799
|
+
.catch(() => {
|
|
800
|
+
// Startup update checks should never interrupt the TUI.
|
|
801
|
+
});
|
|
802
|
+
void checkPixUpdate()
|
|
803
|
+
.then((result) => {
|
|
793
804
|
if (result.status !== "newer")
|
|
794
805
|
return;
|
|
795
806
|
this.showToast(formatPixStartupUpdateDialog(result), "warning", { variant: "dialog" });
|
|
796
|
-
}
|
|
797
|
-
|
|
807
|
+
})
|
|
808
|
+
.catch(() => {
|
|
798
809
|
// Startup update checks should never interrupt the TUI.
|
|
799
|
-
}
|
|
810
|
+
});
|
|
800
811
|
}
|
|
801
812
|
async bindCurrentSession(options) {
|
|
802
813
|
await this.sessionLifecycle.bindCurrentSession(options);
|
|
@@ -889,6 +900,18 @@ export class PiUiExtendApp {
|
|
|
889
900
|
resetSessionView() {
|
|
890
901
|
this.sessionLifecycle.resetSessionView();
|
|
891
902
|
}
|
|
903
|
+
captureSessionView() {
|
|
904
|
+
return {
|
|
905
|
+
entries: [...this.entries],
|
|
906
|
+
eventState: this.sessionEvents.snapshotState(),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
restoreSessionView(view) {
|
|
910
|
+
this.entries.splice(0, this.entries.length, ...view.entries);
|
|
911
|
+
this.sessionEvents.restoreState(view.eventState);
|
|
912
|
+
this.conversationViewport.clear();
|
|
913
|
+
this.workspaceActions.syncUserSessionEntryMetadata();
|
|
914
|
+
}
|
|
892
915
|
loadSessionHistory() {
|
|
893
916
|
void this.sessionEvents.loadSessionHistoryAsync({
|
|
894
917
|
isCancelled: () => !this.running,
|
package/dist/app/cli/update.d.ts
CHANGED
|
@@ -33,12 +33,17 @@ export type PixUpdateCheckOptions = {
|
|
|
33
33
|
packageRoot?: string;
|
|
34
34
|
fetchLatestVersion?: (packageName: string, currentVersion: string, timeoutMs: number) => Promise<string | undefined>;
|
|
35
35
|
};
|
|
36
|
+
export type PiUpdateCheckOptions = PixUpdateCheckOptions & {
|
|
37
|
+
pixPackageRoot?: string;
|
|
38
|
+
};
|
|
36
39
|
export declare function pixUpdateUsage(): string;
|
|
37
40
|
export declare function parsePixUpdateArgs(argv: readonly string[]): PixUpdateCliOptions;
|
|
38
41
|
export declare function getPixPackageVersion(packageRoot?: string): string;
|
|
39
42
|
export declare function checkPixUpdate(options?: PixUpdateCheckOptions): Promise<PixUpdateCheckResult>;
|
|
43
|
+
export declare function checkPiUpdate(options?: PiUpdateCheckOptions): Promise<PixUpdateCheckResult>;
|
|
40
44
|
export declare function formatPixUpdateCheck(result: PixUpdateCheckResult): string;
|
|
41
45
|
export declare function formatPixStartupUpdateDialog(result: PixUpdateCheckResult): string;
|
|
46
|
+
export declare function formatPiStartupUpdateToast(result: PixUpdateCheckResult): string;
|
|
42
47
|
export declare function getPixSelfUpdateCommand(packageName: string, latestVersion?: string, packageRoot?: string): PixSelfUpdateCommand | undefined;
|
|
43
48
|
export declare function runPixUpdateCli(argv?: readonly string[]): Promise<number>;
|
|
44
49
|
declare function runCommand(command: PixSelfUpdateCommand): Promise<void>;
|
package/dist/app/cli/update.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
const DEFAULT_UPDATE_TIMEOUT_MS = 10_000;
|
|
7
8
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
9
|
+
const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
10
|
+
const requireFromUpdateModule = createRequire(import.meta.url);
|
|
8
11
|
const defaultPixUpdateDeps = {
|
|
9
12
|
checkPixUpdate,
|
|
10
13
|
runCommand,
|
|
@@ -53,6 +56,14 @@ export function getPixPackageVersion(packageRoot) {
|
|
|
53
56
|
}
|
|
54
57
|
export async function checkPixUpdate(options = {}) {
|
|
55
58
|
const packageInfo = readPixPackageInfo(options.packageRoot);
|
|
59
|
+
return await checkPackageUpdate(packageInfo, options);
|
|
60
|
+
}
|
|
61
|
+
export async function checkPiUpdate(options = {}) {
|
|
62
|
+
const packageRoot = options.packageRoot ?? findPiPackageRoot(options.pixPackageRoot);
|
|
63
|
+
const packageInfo = readPackageInfo(packageRoot, PI_PACKAGE_NAME);
|
|
64
|
+
return await checkPackageUpdate(packageInfo, options);
|
|
65
|
+
}
|
|
66
|
+
async function checkPackageUpdate(packageInfo, options) {
|
|
56
67
|
const base = {
|
|
57
68
|
packageName: packageInfo.name,
|
|
58
69
|
currentVersion: packageInfo.version,
|
|
@@ -124,6 +135,9 @@ export function formatPixStartupUpdateDialog(result) {
|
|
|
124
135
|
`current: ${result.packageName} v${result.currentVersion}`,
|
|
125
136
|
...(result.latestVersion ? [`latest: ${result.latestVersion}`] : []),
|
|
126
137
|
"",
|
|
138
|
+
"Pix includes the pinned Pi SDK/dependencies used by this renderer.",
|
|
139
|
+
"Updating only the global `pi` CLI is not enough for Pix.",
|
|
140
|
+
"",
|
|
127
141
|
"To update:",
|
|
128
142
|
"1. Exit Pix.",
|
|
129
143
|
"2. Run `pix update` in your shell.",
|
|
@@ -131,6 +145,11 @@ export function formatPixStartupUpdateDialog(result) {
|
|
|
131
145
|
];
|
|
132
146
|
return lines.join("\n");
|
|
133
147
|
}
|
|
148
|
+
export function formatPiStartupUpdateToast(result) {
|
|
149
|
+
return result.latestVersion
|
|
150
|
+
? `Pi ${result.latestVersion} is available; Pix bundles Pi ${result.currentVersion}. Waiting for a matching Pix update.`
|
|
151
|
+
: `Pi update detected; Pix bundles Pi ${result.currentVersion}. Waiting for a matching Pix update.`;
|
|
152
|
+
}
|
|
134
153
|
export function getPixSelfUpdateCommand(packageName, latestVersion, packageRoot = readPixPackageInfo().packageRoot) {
|
|
135
154
|
if (!packageRootLooksPackageManaged(packageRoot))
|
|
136
155
|
return undefined;
|
|
@@ -191,9 +210,12 @@ export async function runPixUpdateCli(argv = process.argv.slice(2)) {
|
|
|
191
210
|
}
|
|
192
211
|
}
|
|
193
212
|
function readPixPackageInfo(packageRoot = findPixPackageRoot()) {
|
|
213
|
+
return readPackageInfo(packageRoot, "pi-ui-extend");
|
|
214
|
+
}
|
|
215
|
+
function readPackageInfo(packageRoot, fallbackName) {
|
|
194
216
|
const packageJsonPath = join(packageRoot, "package.json");
|
|
195
217
|
const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
196
|
-
const name = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() :
|
|
218
|
+
const name = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : fallbackName;
|
|
197
219
|
const version = typeof raw.version === "string" && raw.version.trim() ? raw.version.trim() : "0.0.0";
|
|
198
220
|
return {
|
|
199
221
|
name,
|
|
@@ -214,6 +236,12 @@ function findPixPackageRoot() {
|
|
|
214
236
|
currentDir = nextDir;
|
|
215
237
|
}
|
|
216
238
|
}
|
|
239
|
+
function findPiPackageRoot(pixPackageRoot = readPixPackageInfo().packageRoot) {
|
|
240
|
+
const packageJsonPath = requireFromUpdateModule.resolve(`${PI_PACKAGE_NAME}/package.json`, {
|
|
241
|
+
paths: [pixPackageRoot],
|
|
242
|
+
});
|
|
243
|
+
return dirname(packageJsonPath);
|
|
244
|
+
}
|
|
217
245
|
async function fetchLatestNpmVersion(packageName, currentVersion, timeoutMs) {
|
|
218
246
|
const response = await fetch(`${NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`, {
|
|
219
247
|
headers: {
|
|
@@ -89,6 +89,8 @@ type AntigravityQuotaAccount = {
|
|
|
89
89
|
readonly email?: string;
|
|
90
90
|
readonly refreshToken: string;
|
|
91
91
|
readonly accessToken?: string;
|
|
92
|
+
readonly clientId?: string;
|
|
93
|
+
readonly clientSecret?: string;
|
|
92
94
|
readonly cachedQuota?: AntigravityCachedQuota;
|
|
93
95
|
readonly cachedQuotaUpdatedAt?: number;
|
|
94
96
|
readonly projectId: string;
|
|
@@ -8,6 +8,8 @@ const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
|
8
8
|
const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
9
9
|
const ZHIPU_QUOTA_URL = "https://bigmodel.cn/api/monitor/usage/quota/limit";
|
|
10
10
|
const GOOGLE_QUOTA_API_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
|
11
|
+
const GOOGLE_TOKEN_REFRESH_URL = "https://oauth2.googleapis.com/token";
|
|
12
|
+
const GOOGLE_ANTIGRAVITY_USER_AGENT = "antigravity/1.11.9 windows/amd64";
|
|
11
13
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
12
14
|
const DAY_SECONDS = 86_400;
|
|
13
15
|
const HOUR_SECONDS = 3_600;
|
|
@@ -383,11 +385,11 @@ export function resolveAntigravityQuotaModelKey(model) {
|
|
|
383
385
|
}
|
|
384
386
|
export function googleAntigravityUsageStatusFromResponse(data, descriptor, now = Date.now()) {
|
|
385
387
|
const quotaInfo = data.models[descriptor.quotaModelKey]?.quotaInfo;
|
|
386
|
-
if (!quotaInfo
|
|
388
|
+
if (!quotaInfo)
|
|
387
389
|
return undefined;
|
|
388
390
|
const resetAt = parseResetTime(quotaInfo.resetTime, now);
|
|
389
391
|
const window = {
|
|
390
|
-
remainingPercent:
|
|
392
|
+
remainingPercent: quotaRemainingPercent(quotaInfo),
|
|
391
393
|
resetAt,
|
|
392
394
|
windowSeconds: Math.max(0, Math.round((resetAt - now) / 1000)),
|
|
393
395
|
};
|
|
@@ -404,13 +406,8 @@ export function googleAntigravityUsageStatusFromResponse(data, descriptor, now =
|
|
|
404
406
|
}
|
|
405
407
|
async function queryGoogleAntigravityModelUsage(descriptor) {
|
|
406
408
|
const now = Date.now();
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
return googleAntigravityUsageStatusFromResponse(cachedResponse, descriptor, now);
|
|
410
|
-
if (!descriptor.account.accessToken)
|
|
411
|
-
return undefined;
|
|
412
|
-
const response = await fetchGoogleAntigravityQuota(descriptor.account.accessToken, descriptor.account.projectId);
|
|
413
|
-
return googleAntigravityUsageStatusFromResponse(response, descriptor);
|
|
409
|
+
const response = await fetchGoogleAntigravityQuotaForAccount(descriptor.account, now);
|
|
410
|
+
return googleAntigravityUsageStatusFromResponse(response, descriptor, now);
|
|
414
411
|
}
|
|
415
412
|
const GOOGLE_ACCOUNT_QUOTA_WINDOWS = [
|
|
416
413
|
{ label: "Claude Opus", quotaModelKey: "claude-opus-4-6-thinking" },
|
|
@@ -424,12 +421,8 @@ async function queryGoogleAntigravityAccountUsage(now) {
|
|
|
424
421
|
const results = await Promise.all(accounts.map(async (account) => {
|
|
425
422
|
const accountLabel = account.email ?? maskCredential(account.refreshToken);
|
|
426
423
|
try {
|
|
427
|
-
const response =
|
|
428
|
-
const windows =
|
|
429
|
-
if (windows.length === 0 && account.accessToken) {
|
|
430
|
-
const liveResponse = await fetchGoogleAntigravityQuota(account.accessToken, account.projectId);
|
|
431
|
-
windows.push(...googleAccountWindowsFromResponse(liveResponse, now));
|
|
432
|
-
}
|
|
424
|
+
const response = await fetchGoogleAntigravityQuotaForAccount(account, now);
|
|
425
|
+
const windows = googleAccountWindowsFromResponse(response, now);
|
|
433
426
|
return {
|
|
434
427
|
account: accountLabel,
|
|
435
428
|
windows,
|
|
@@ -456,12 +449,14 @@ function readAllAntigravityQuotaAccounts() {
|
|
|
456
449
|
const credential = readPiAuthSync().antigravity;
|
|
457
450
|
if (!credential)
|
|
458
451
|
return [];
|
|
452
|
+
const credentialClient = getGoogleOAuthClientCredentials(credential);
|
|
459
453
|
const accounts = storedAntigravityAccounts(credential);
|
|
460
454
|
if (accounts.length > 0) {
|
|
461
455
|
const activeIndex = clampAccountIndex(credential.activeIndex, accounts.length);
|
|
462
456
|
const activeAccess = antigravityAccessFromCredential(credential);
|
|
463
457
|
return accounts.map((account, accountIndex) => antigravityQuotaAccount(account, {
|
|
464
458
|
...(credential.email ? { fallbackEmail: credential.email } : {}),
|
|
459
|
+
...(credentialClient ? { clientCredentials: credentialClient } : {}),
|
|
465
460
|
...(accountIndex === activeIndex && activeAccess ? { accessToken: activeAccess.accessToken } : {}),
|
|
466
461
|
accountIndex,
|
|
467
462
|
accountCount: accounts.length,
|
|
@@ -471,6 +466,7 @@ function readAllAntigravityQuotaAccounts() {
|
|
|
471
466
|
const fallbackAccess = antigravityAccessFromCredential(credential);
|
|
472
467
|
const account = fallbackAccount ? antigravityQuotaAccount(fallbackAccount, {
|
|
473
468
|
...(credential.email ? { fallbackEmail: credential.email } : {}),
|
|
469
|
+
...(credentialClient ? { clientCredentials: credentialClient } : {}),
|
|
474
470
|
...(fallbackAccess ? { accessToken: fallbackAccess.accessToken } : {}),
|
|
475
471
|
}) : undefined;
|
|
476
472
|
return account ? [account] : [];
|
|
@@ -483,9 +479,41 @@ function readPiAuthSync() {
|
|
|
483
479
|
return {};
|
|
484
480
|
}
|
|
485
481
|
}
|
|
482
|
+
function getAccountRefreshToken(account) {
|
|
483
|
+
if (account.refreshToken)
|
|
484
|
+
return account.refreshToken;
|
|
485
|
+
if (!account.refresh)
|
|
486
|
+
return undefined;
|
|
487
|
+
return splitAntigravityRefresh(account.refresh).refreshToken;
|
|
488
|
+
}
|
|
489
|
+
function stringProperty(source, keys) {
|
|
490
|
+
if (!source || typeof source !== "object")
|
|
491
|
+
return undefined;
|
|
492
|
+
const record = source;
|
|
493
|
+
for (const key of keys) {
|
|
494
|
+
const value = record[key];
|
|
495
|
+
if (typeof value === "string" && value)
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
function getGoogleOAuthClientCredentials(...sources) {
|
|
501
|
+
for (const source of sources) {
|
|
502
|
+
const nested = source && typeof source === "object"
|
|
503
|
+
? source.oauthClient
|
|
504
|
+
: undefined;
|
|
505
|
+
const nestedClientId = stringProperty(nested, ["clientId", "client_id", "id"]);
|
|
506
|
+
const nestedClientSecret = stringProperty(nested, ["clientSecret", "client_secret", "secret"]);
|
|
507
|
+
const clientId = nestedClientId ?? stringProperty(source, ["clientId", "client_id", "googleClientId", "google_client_id", "oauthClientId", "oauth_client_id"]);
|
|
508
|
+
const clientSecret = nestedClientSecret ?? stringProperty(source, ["clientSecret", "client_secret", "googleClientSecret", "google_client_secret", "oauthClientSecret", "oauth_client_secret"]);
|
|
509
|
+
if (clientId)
|
|
510
|
+
return { clientId, ...(clientSecret ? { clientSecret } : {}) };
|
|
511
|
+
}
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
486
514
|
function storedAntigravityAccounts(credential) {
|
|
487
515
|
return Array.isArray(credential.accounts)
|
|
488
|
-
? credential.accounts.filter((account) => account.enabled !== false && !!account
|
|
516
|
+
? credential.accounts.filter((account) => account.enabled !== false && !!getAccountRefreshToken(account))
|
|
489
517
|
: [];
|
|
490
518
|
}
|
|
491
519
|
function antigravityAccountFromCredential(credential) {
|
|
@@ -506,16 +534,19 @@ function antigravityAccountFromCredential(credential) {
|
|
|
506
534
|
};
|
|
507
535
|
}
|
|
508
536
|
function antigravityQuotaAccount(account, options = {}) {
|
|
509
|
-
const refreshToken = account
|
|
537
|
+
const refreshToken = getAccountRefreshToken(account);
|
|
510
538
|
if (!refreshToken)
|
|
511
539
|
return undefined;
|
|
512
540
|
const email = account.email || options.fallbackEmail;
|
|
513
541
|
const projectId = account.projectId || account.managedProjectId || DEFAULT_ANTIGRAVITY_PROJECT_ID;
|
|
542
|
+
const clientCredentials = getGoogleOAuthClientCredentials(account, options.clientCredentials);
|
|
514
543
|
return {
|
|
515
544
|
refreshToken,
|
|
516
545
|
projectId,
|
|
517
546
|
cacheKey: email ? email.toLowerCase() : shortHash(refreshToken),
|
|
518
547
|
...(options.accessToken ? { accessToken: options.accessToken } : {}),
|
|
548
|
+
...(clientCredentials ? { clientId: clientCredentials.clientId } : {}),
|
|
549
|
+
...(clientCredentials?.clientSecret ? { clientSecret: clientCredentials.clientSecret } : {}),
|
|
519
550
|
...(account.cachedQuota ? { cachedQuota: account.cachedQuota } : {}),
|
|
520
551
|
...(typeof account.cachedQuotaUpdatedAt === "number" ? { cachedQuotaUpdatedAt: account.cachedQuotaUpdatedAt } : {}),
|
|
521
552
|
...(email ? { email } : {}),
|
|
@@ -590,13 +621,49 @@ function clampAccountIndex(index, accountCount) {
|
|
|
590
621
|
function shortHash(value) {
|
|
591
622
|
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
592
623
|
}
|
|
624
|
+
async function fetchGoogleAntigravityQuotaForAccount(account, now = Date.now()) {
|
|
625
|
+
if (account.accessToken)
|
|
626
|
+
return await fetchGoogleAntigravityQuota(account.accessToken, account.projectId);
|
|
627
|
+
if (account.clientId) {
|
|
628
|
+
const { accessToken } = await refreshGoogleAntigravityAccessToken(account);
|
|
629
|
+
return await fetchGoogleAntigravityQuota(accessToken, account.projectId);
|
|
630
|
+
}
|
|
631
|
+
const cachedResponse = googleQuotaResponseFromCachedQuota(account.cachedQuota, account.cachedQuotaUpdatedAt, now);
|
|
632
|
+
if (cachedResponse)
|
|
633
|
+
return cachedResponse;
|
|
634
|
+
throw new Error("Missing Google OAuth client credentials, cannot query live Antigravity quota.");
|
|
635
|
+
}
|
|
636
|
+
async function refreshGoogleAntigravityAccessToken(account) {
|
|
637
|
+
if (!account.clientId)
|
|
638
|
+
throw new Error("Missing Google OAuth client id, cannot refresh Antigravity access token.");
|
|
639
|
+
const params = new URLSearchParams({
|
|
640
|
+
client_id: account.clientId,
|
|
641
|
+
refresh_token: account.refreshToken,
|
|
642
|
+
grant_type: "refresh_token",
|
|
643
|
+
});
|
|
644
|
+
if (account.clientSecret)
|
|
645
|
+
params.set("client_secret", account.clientSecret);
|
|
646
|
+
const response = await fetchWithTimeout(GOOGLE_TOKEN_REFRESH_URL, {
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
649
|
+
body: params,
|
|
650
|
+
});
|
|
651
|
+
if (!response.ok) {
|
|
652
|
+
const errorText = await response.text();
|
|
653
|
+
throw new Error(`Google token refresh failed (${response.status}): ${errorText}`);
|
|
654
|
+
}
|
|
655
|
+
const data = await response.json();
|
|
656
|
+
if (!data.access_token)
|
|
657
|
+
throw new Error("Google token refresh did not return an access token.");
|
|
658
|
+
return { accessToken: data.access_token };
|
|
659
|
+
}
|
|
593
660
|
async function fetchGoogleAntigravityQuota(accessToken, projectId) {
|
|
594
661
|
const response = await fetchWithTimeout(GOOGLE_QUOTA_API_URL, {
|
|
595
662
|
method: "POST",
|
|
596
663
|
headers: {
|
|
597
664
|
"Content-Type": "application/json",
|
|
598
665
|
Authorization: `Bearer ${accessToken}`,
|
|
599
|
-
"User-Agent":
|
|
666
|
+
"User-Agent": GOOGLE_ANTIGRAVITY_USER_AGENT,
|
|
600
667
|
},
|
|
601
668
|
body: JSON.stringify({ project: projectId }),
|
|
602
669
|
});
|
|
@@ -757,16 +824,19 @@ function accountWindowFromRateLimit(window, now) {
|
|
|
757
824
|
}
|
|
758
825
|
function googleAccountWindowFromResponse(data, label, quotaModelKey, now) {
|
|
759
826
|
const quotaInfo = data.models[quotaModelKey]?.quotaInfo;
|
|
760
|
-
if (!quotaInfo
|
|
827
|
+
if (!quotaInfo)
|
|
761
828
|
return undefined;
|
|
762
829
|
const resetAt = parseResetTime(quotaInfo.resetTime, now);
|
|
763
830
|
return {
|
|
764
831
|
label,
|
|
765
|
-
remainingPercent:
|
|
832
|
+
remainingPercent: quotaRemainingPercent(quotaInfo),
|
|
766
833
|
resetAt,
|
|
767
834
|
windowSeconds: Math.max(0, Math.round((resetAt - now) / 1000)),
|
|
768
835
|
};
|
|
769
836
|
}
|
|
837
|
+
function quotaRemainingPercent(quotaInfo) {
|
|
838
|
+
return clampPercent(Math.round((Number.isFinite(quotaInfo.remainingFraction) ? quotaInfo.remainingFraction : 0) * 100));
|
|
839
|
+
}
|
|
770
840
|
function googleAccountWindowsFromResponse(data, now) {
|
|
771
841
|
return GOOGLE_ACCOUNT_QUOTA_WINDOWS
|
|
772
842
|
.map((window) => googleAccountWindowFromResponse(data, window.label, window.quotaModelKey, now))
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { AgentSessionEvent, AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ConversationViewport } from "../rendering/conversation-viewport.js";
|
|
3
|
-
import { type LoadOlderSessionHistoryOptions } from "./session-history.js";
|
|
3
|
+
import { type LoadOlderSessionHistoryOptions, type SessionHistoryOlderLoader } from "./session-history.js";
|
|
4
4
|
import type { Entry, SessionActivity } from "../types.js";
|
|
5
5
|
import type { WorkspaceMutation, WorkspaceMutationPreparation } from "../workspace/workspace-undo.js";
|
|
6
|
+
export type AppSessionEventControllerState = {
|
|
7
|
+
toolEntryIdsByCallId: Map<string, string>;
|
|
8
|
+
toolMutationPreparationsByCallId: Map<string, {
|
|
9
|
+
userEntryId: string;
|
|
10
|
+
args: unknown;
|
|
11
|
+
preparation?: WorkspaceMutationPreparation;
|
|
12
|
+
}>;
|
|
13
|
+
olderHistoryLoader: SessionHistoryOlderLoader | undefined;
|
|
14
|
+
currentUserEntryId: string | undefined;
|
|
15
|
+
currentAssistantEntryId: string | undefined;
|
|
16
|
+
currentThinkingEntryId: string | undefined;
|
|
17
|
+
assistantTextBuffer: string;
|
|
18
|
+
entryRenderVersions: Map<string, number>;
|
|
19
|
+
};
|
|
6
20
|
export type AppSessionEventControllerHost = {
|
|
7
21
|
readonly entries: Entry[];
|
|
8
22
|
runtime(): AgentSessionRuntime | undefined;
|
|
@@ -45,6 +59,8 @@ export declare class AppSessionEventController {
|
|
|
45
59
|
private currentThinkingEntryId;
|
|
46
60
|
private assistantTextBuffer;
|
|
47
61
|
constructor(host: AppSessionEventControllerHost);
|
|
62
|
+
snapshotState(): AppSessionEventControllerState;
|
|
63
|
+
restoreState(state: AppSessionEventControllerState): void;
|
|
48
64
|
reset(): void;
|
|
49
65
|
loadSessionHistory(): void;
|
|
50
66
|
loadSessionHistoryAsync(options: {
|
|
@@ -20,6 +20,34 @@ export class AppSessionEventController {
|
|
|
20
20
|
constructor(host) {
|
|
21
21
|
this.host = host;
|
|
22
22
|
}
|
|
23
|
+
snapshotState() {
|
|
24
|
+
return {
|
|
25
|
+
toolEntryIdsByCallId: new Map(this.toolEntryIdsByCallId),
|
|
26
|
+
toolMutationPreparationsByCallId: new Map(this.toolMutationPreparationsByCallId),
|
|
27
|
+
olderHistoryLoader: this.olderHistoryLoader,
|
|
28
|
+
currentUserEntryId: this.currentUserEntryId,
|
|
29
|
+
currentAssistantEntryId: this.currentAssistantEntryId,
|
|
30
|
+
currentThinkingEntryId: this.currentThinkingEntryId,
|
|
31
|
+
assistantTextBuffer: this.assistantTextBuffer,
|
|
32
|
+
entryRenderVersions: new Map(this.entryRenderVersions),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
restoreState(state) {
|
|
36
|
+
this.toolEntryIdsByCallId.clear();
|
|
37
|
+
for (const [key, value] of state.toolEntryIdsByCallId)
|
|
38
|
+
this.toolEntryIdsByCallId.set(key, value);
|
|
39
|
+
this.toolMutationPreparationsByCallId.clear();
|
|
40
|
+
for (const [key, value] of state.toolMutationPreparationsByCallId)
|
|
41
|
+
this.toolMutationPreparationsByCallId.set(key, value);
|
|
42
|
+
this.olderHistoryLoader = state.olderHistoryLoader;
|
|
43
|
+
this.currentUserEntryId = state.currentUserEntryId;
|
|
44
|
+
this.currentAssistantEntryId = state.currentAssistantEntryId;
|
|
45
|
+
this.currentThinkingEntryId = state.currentThinkingEntryId;
|
|
46
|
+
this.assistantTextBuffer = state.assistantTextBuffer;
|
|
47
|
+
this.entryRenderVersions.clear();
|
|
48
|
+
for (const [key, value] of state.entryRenderVersions)
|
|
49
|
+
this.entryRenderVersions.set(key, value);
|
|
50
|
+
}
|
|
23
51
|
reset() {
|
|
24
52
|
this.toolEntryIdsByCallId.clear();
|
|
25
53
|
this.toolMutationPreparationsByCallId.clear();
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type AgentSession, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { BindCurrentSessionOptions } from "./session-lifecycle-controller.js";
|
|
3
|
+
import type { AppSessionEventControllerState } from "./session-event-controller.js";
|
|
3
4
|
import type { InputEditorDraftState } from "../../input-editor.js";
|
|
4
5
|
import type { AppBlinkController } from "../screen/blink-controller.js";
|
|
5
6
|
import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessage } from "../types.js";
|
|
7
|
+
type TabSessionView = {
|
|
8
|
+
entries: Entry[];
|
|
9
|
+
eventState: AppSessionEventControllerState;
|
|
10
|
+
};
|
|
6
11
|
export type TabInputState = InputEditorDraftState;
|
|
7
12
|
export type AppTabsControllerHost = {
|
|
8
13
|
readonly options: AppOptions;
|
|
@@ -24,6 +29,8 @@ export type AppTabsControllerHost = {
|
|
|
24
29
|
render: () => void;
|
|
25
30
|
lazyOlderHistory?: boolean;
|
|
26
31
|
}): Promise<boolean>;
|
|
32
|
+
captureSessionView?(): TabSessionView;
|
|
33
|
+
restoreSessionView?(view: TabSessionView): void;
|
|
27
34
|
syncUserSessionEntryMetadata(): void;
|
|
28
35
|
captureInputState(): TabInputState;
|
|
29
36
|
restoreInputState(state: TabInputState): void;
|
|
@@ -44,6 +51,7 @@ export declare class AppTabsController {
|
|
|
44
51
|
private readonly historyReloadTimersByTabId;
|
|
45
52
|
private readonly inputStatesByTabId;
|
|
46
53
|
private readonly deferredUserMessagesByTabId;
|
|
54
|
+
private readonly sessionViewsByTabId;
|
|
47
55
|
private readonly tabIdsNeedingHistoryReload;
|
|
48
56
|
private activeTabId;
|
|
49
57
|
private pendingActiveTabId;
|
|
@@ -83,6 +91,7 @@ export declare class AppTabsController {
|
|
|
83
91
|
private activeTab;
|
|
84
92
|
private clearStartupTabPlaceholders;
|
|
85
93
|
private storeActiveRuntime;
|
|
94
|
+
private storeActiveSessionView;
|
|
86
95
|
private setRuntimeForTab;
|
|
87
96
|
private deleteRuntimeForTab;
|
|
88
97
|
private clearRuntimeSubscriptions;
|
|
@@ -138,3 +147,4 @@ export declare class AppTabsController {
|
|
|
138
147
|
private preservedSessionPaths;
|
|
139
148
|
private maxProjectSessions;
|
|
140
149
|
}
|
|
150
|
+
export {};
|
|
@@ -12,6 +12,7 @@ const BACKGROUND_PREWARM_TAB_LIMIT = 2;
|
|
|
12
12
|
const TAB_ATTENTION_BLINK_KEY = "tab-attention";
|
|
13
13
|
const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
|
|
14
14
|
const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
|
|
15
|
+
const SESSION_TITLE_HEAD_SCAN_MAX_BYTES = 256 * 1024;
|
|
15
16
|
const SESSION_TITLE_SCAN_MAX_BYTES = 2 * 1024 * 1024;
|
|
16
17
|
export class AppTabsController {
|
|
17
18
|
host;
|
|
@@ -23,6 +24,7 @@ export class AppTabsController {
|
|
|
23
24
|
historyReloadTimersByTabId = new Map();
|
|
24
25
|
inputStatesByTabId = new Map();
|
|
25
26
|
deferredUserMessagesByTabId = new Map();
|
|
27
|
+
sessionViewsByTabId = new Map();
|
|
26
28
|
tabIdsNeedingHistoryReload = new Set();
|
|
27
29
|
activeTabId;
|
|
28
30
|
pendingActiveTabId;
|
|
@@ -576,6 +578,7 @@ export class AppTabsController {
|
|
|
576
578
|
const previousRuntime = runtime;
|
|
577
579
|
const previousTargetActivity = target.activity;
|
|
578
580
|
this.storeActiveRuntime(runtime);
|
|
581
|
+
this.storeActiveSessionView();
|
|
579
582
|
this.storeActiveInputState();
|
|
580
583
|
this.storeActiveDeferredUserMessages();
|
|
581
584
|
this.activeTabId = target.id;
|
|
@@ -584,8 +587,6 @@ export class AppTabsController {
|
|
|
584
587
|
this.clearTabAttention(target);
|
|
585
588
|
this.restoreInputState(target.id);
|
|
586
589
|
this.host.closeMenusForTabSwitch?.();
|
|
587
|
-
this.host.resetSessionView();
|
|
588
|
-
this.restoreDeferredUserMessages(target.id);
|
|
589
590
|
this.host.setStatus("switching tab");
|
|
590
591
|
this.host.setSessionActivity("thinking");
|
|
591
592
|
this.host.render();
|
|
@@ -633,7 +634,18 @@ export class AppTabsController {
|
|
|
633
634
|
this.restoreInputState(target.id);
|
|
634
635
|
void this.saveTabs();
|
|
635
636
|
this.scheduleTabPrewarm();
|
|
636
|
-
|
|
637
|
+
const cachedView = this.sessionViewsByTabId.get(target.id);
|
|
638
|
+
if (cachedView && this.host.restoreSessionView) {
|
|
639
|
+
this.host.restoreSessionView(cachedView);
|
|
640
|
+
this.restoreDeferredUserMessages(target.id);
|
|
641
|
+
this.host.setSessionStatus(targetRuntime.session);
|
|
642
|
+
this.host.setSessionActivity(this.sessionActivity(targetRuntime.session));
|
|
643
|
+
this.host.render();
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
await this.loadActiveSessionHistory(targetRuntime);
|
|
647
|
+
this.tabIdsNeedingHistoryReload.delete(target.id);
|
|
648
|
+
}
|
|
637
649
|
this.scheduleDelayedHistoryReload(target.id, targetRuntime);
|
|
638
650
|
}
|
|
639
651
|
async closeTab(tabId) {
|
|
@@ -809,6 +821,11 @@ export class AppTabsController {
|
|
|
809
821
|
return;
|
|
810
822
|
this.setRuntimeForTab(this.activeTabId, runtime);
|
|
811
823
|
}
|
|
824
|
+
storeActiveSessionView() {
|
|
825
|
+
if (!this.activeTabId || !this.host.captureSessionView)
|
|
826
|
+
return;
|
|
827
|
+
this.sessionViewsByTabId.set(this.activeTabId, this.host.captureSessionView());
|
|
828
|
+
}
|
|
812
829
|
setRuntimeForTab(tabId, runtime) {
|
|
813
830
|
this.runtimesByTabId.set(tabId, runtime);
|
|
814
831
|
this.observeRuntimeForTab(tabId, runtime);
|
|
@@ -816,6 +833,7 @@ export class AppTabsController {
|
|
|
816
833
|
deleteRuntimeForTab(tabId) {
|
|
817
834
|
this.runtimesByTabId.delete(tabId);
|
|
818
835
|
this.runtimeLoadsByTabId.delete(tabId);
|
|
836
|
+
this.sessionViewsByTabId.delete(tabId);
|
|
819
837
|
this.clearRuntimeRefreshTimers(tabId);
|
|
820
838
|
this.clearHistoryReloadTimers(tabId);
|
|
821
839
|
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
@@ -900,11 +918,15 @@ export class AppTabsController {
|
|
|
900
918
|
return;
|
|
901
919
|
if (tabId !== this.activeTabId || this.pendingActiveTabId !== undefined)
|
|
902
920
|
return;
|
|
921
|
+
if (this.sessionActivity(runtime.session) === "running") {
|
|
922
|
+
this.clearHistoryReloadTimers(tabId);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
903
925
|
this.clearHistoryReloadTimers(tabId);
|
|
904
926
|
for (const delayMs of [150, 1000, 3000]) {
|
|
905
927
|
const timer = setTimeout(() => {
|
|
906
928
|
this.historyReloadTimersByTabId.get(tabId)?.delete(timer);
|
|
907
|
-
void this.reloadActiveTabHistoryIfNeeded(tabId, runtime
|
|
929
|
+
void this.reloadActiveTabHistoryIfNeeded(tabId, runtime);
|
|
908
930
|
}, delayMs);
|
|
909
931
|
timer.unref?.();
|
|
910
932
|
let timers = this.historyReloadTimersByTabId.get(tabId);
|
|
@@ -915,13 +937,15 @@ export class AppTabsController {
|
|
|
915
937
|
timers.add(timer);
|
|
916
938
|
}
|
|
917
939
|
}
|
|
918
|
-
async reloadActiveTabHistoryIfNeeded(tabId, runtime
|
|
940
|
+
async reloadActiveTabHistoryIfNeeded(tabId, runtime) {
|
|
919
941
|
if (tabId !== this.activeTabId || this.pendingActiveTabId !== undefined || this.host.runtime() !== runtime)
|
|
920
942
|
return;
|
|
921
943
|
if (!this.tabIdsNeedingHistoryReload.has(tabId))
|
|
922
944
|
return;
|
|
945
|
+
if (this.sessionActivity(runtime.session) === "running")
|
|
946
|
+
return;
|
|
923
947
|
await this.loadActiveSessionHistory(runtime);
|
|
924
|
-
if (
|
|
948
|
+
if (tabId === this.activeTabId && this.host.runtime() === runtime) {
|
|
925
949
|
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
926
950
|
}
|
|
927
951
|
}
|
|
@@ -1027,6 +1051,7 @@ export class AppTabsController {
|
|
|
1027
1051
|
this.clearRuntimeSubscriptions();
|
|
1028
1052
|
this.inputStatesByTabId.clear();
|
|
1029
1053
|
this.deferredUserMessagesByTabId.clear();
|
|
1054
|
+
this.sessionViewsByTabId.clear();
|
|
1030
1055
|
const seen = new Set();
|
|
1031
1056
|
for (const tab of tabs) {
|
|
1032
1057
|
const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
|
|
@@ -1505,28 +1530,14 @@ async function readLatestSessionTitle(sessionPath) {
|
|
|
1505
1530
|
const { size } = await file.stat();
|
|
1506
1531
|
if (size <= 0)
|
|
1507
1532
|
return undefined;
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const line = lines[index]?.trim();
|
|
1517
|
-
if (!line)
|
|
1518
|
-
continue;
|
|
1519
|
-
let parsed;
|
|
1520
|
-
try {
|
|
1521
|
-
parsed = JSON.parse(line);
|
|
1522
|
-
}
|
|
1523
|
-
catch {
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
if (!isRecord(parsed) || parsed.type !== "session_info" || typeof parsed.name !== "string")
|
|
1527
|
-
continue;
|
|
1528
|
-
return validSessionTitle(parsed.name);
|
|
1529
|
-
}
|
|
1533
|
+
const tailByteCount = Math.min(size, SESSION_TITLE_SCAN_MAX_BYTES);
|
|
1534
|
+
const tailTitle = await readSessionTitleChunk(file, size - tailByteCount, tailByteCount, { dropFirstLine: size > tailByteCount });
|
|
1535
|
+
if (tailTitle)
|
|
1536
|
+
return tailTitle;
|
|
1537
|
+
if (size <= tailByteCount)
|
|
1538
|
+
return undefined;
|
|
1539
|
+
const headByteCount = Math.min(size - tailByteCount, SESSION_TITLE_HEAD_SCAN_MAX_BYTES);
|
|
1540
|
+
return await readSessionTitleChunk(file, 0, headByteCount, { dropLastLine: headByteCount < size });
|
|
1530
1541
|
}
|
|
1531
1542
|
catch {
|
|
1532
1543
|
return undefined;
|
|
@@ -1534,6 +1545,32 @@ async function readLatestSessionTitle(sessionPath) {
|
|
|
1534
1545
|
finally {
|
|
1535
1546
|
await file?.close();
|
|
1536
1547
|
}
|
|
1548
|
+
}
|
|
1549
|
+
async function readSessionTitleChunk(file, position, byteCount, options = {}) {
|
|
1550
|
+
if (byteCount <= 0)
|
|
1551
|
+
return undefined;
|
|
1552
|
+
const buffer = Buffer.alloc(byteCount);
|
|
1553
|
+
await file.read(buffer, 0, byteCount, position);
|
|
1554
|
+
const lines = buffer.toString("utf8").split("\n");
|
|
1555
|
+
if (options.dropFirstLine)
|
|
1556
|
+
lines.shift();
|
|
1557
|
+
if (options.dropLastLine)
|
|
1558
|
+
lines.pop();
|
|
1559
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1560
|
+
const line = lines[index]?.trim();
|
|
1561
|
+
if (!line)
|
|
1562
|
+
continue;
|
|
1563
|
+
let parsed;
|
|
1564
|
+
try {
|
|
1565
|
+
parsed = JSON.parse(line);
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
if (!isRecord(parsed) || parsed.type !== "session_info" || typeof parsed.name !== "string")
|
|
1571
|
+
continue;
|
|
1572
|
+
return validSessionTitle(parsed.name);
|
|
1573
|
+
}
|
|
1537
1574
|
return undefined;
|
|
1538
1575
|
}
|
|
1539
1576
|
function validSessionTitle(value) {
|
|
@@ -41,9 +41,6 @@
|
|
|
41
41
|
"@earendil-works/pi-ai": "*",
|
|
42
42
|
"@earendil-works/pi-coding-agent": "*",
|
|
43
43
|
"@earendil-works/pi-tui": "*",
|
|
44
|
-
"@mariozechner/pi-ai": "*",
|
|
45
|
-
"@mariozechner/pi-coding-agent": "*",
|
|
46
|
-
"@mariozechner/pi-tui": "*",
|
|
47
44
|
"typebox": "*"
|
|
48
45
|
},
|
|
49
46
|
"devDependencies": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateToWidth, visibleWidth } from "@
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { AgentState } from "./lib.js";
|
|
3
3
|
import { modelName, plural, statusGlyph, statusLabel } from "./format.js";
|
|
4
4
|
import type { AgentTaskPreview, SubagentRunRenderDetails } from "./types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
5
5
|
import { readResult, resolveSubagentAgentRunDir, validateBasename } from "../lib.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { ExtensionAPI } from "@
|
|
4
|
-
import { Type } from "@
|
|
5
|
-
import { Text } from "@
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
7
7
|
import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, Semaphore, SpawnedAgent } from "../lib.js";
|
|
8
8
|
import {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
3
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
5
5
|
import { getRunState, resolveSubagentRunDir, validateBasename } from "../lib.js";
|
|
6
6
|
import { INLINE_RENDERING } from "../constants.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
4
4
|
import { getRunState, resolveSubagentRunDir, stopAgents, validateBasename, validateStopSignal } from "../lib.js";
|
|
5
5
|
import { INLINE_RENDERING } from "../constants.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
3
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { asyncSubagentToolDescriptions } from "../../tool-descriptions.js";
|
|
5
5
|
import { hasIndexedProjectRoot } from "../../lib/project.js";
|
|
6
6
|
import type { AgentCompletionHandler } from "../lib.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
|
|
4
4
|
import { resolveSubagentRunDir, validateBasename } from "../lib.js";
|
|
5
5
|
import { INLINE_RENDERING } from "../constants.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
2
|
-
import type { AutocompleteItem } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui"
|
|
3
3
|
import type { DcpState } from "./state.js"
|
|
4
4
|
import type { DcpConfig } from "./config.js"
|
|
5
5
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox"
|
|
6
|
-
import type { ExtensionAPI } from "@
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
7
7
|
import type { DcpState } from "./state.js"
|
|
8
8
|
import type { DcpConfig } from "./config.js"
|
|
9
9
|
import { clearDcpNudgeAnchors } from "./pruner.js"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Dynamic Context Pruning (DCP) — module entry point for pi-tools-suite
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
6
6
|
import { loadConfig } from "./config.js"
|
|
7
7
|
import {
|
|
8
8
|
createState,
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { withFileMutationQueue } from "@
|
|
5
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { isPathInside } from "./path-utils";
|
|
7
7
|
|
|
8
8
|
export interface ApplyPatchResult {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ExtensionContext,
|
|
12
12
|
type ToolDefinition,
|
|
13
13
|
type ToolRenderResultOptions,
|
|
14
|
-
} from "@
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { realpath } from "node:fs/promises";
|
|
16
16
|
import { resolve } from "node:path";
|
|
17
17
|
import { Type, type TSchema } from "typebox";
|
|
@@ -145,7 +145,7 @@ function appendWorkflowReminder(text: string, op: Op, state: TaskState): string
|
|
|
145
145
|
const lines = [text];
|
|
146
146
|
if (op.kind === "create" || op.kind === "batch_create") {
|
|
147
147
|
lines.push(
|
|
148
|
-
"Reminder: if this is a multi-step task, include a final todo item for the user-facing final report before completion. Give that final-report todo an explicit description/acceptance criteria: summarize changed files and behavior, list verification commands/results, mention any remaining manual action, and never replace the user-facing report with a compression/housekeeping note.",
|
|
148
|
+
"Reminder: if this is a multi-step task, include a final todo item for the user-facing final report before completion. Give that final-report todo an explicit description/acceptance criteria: summarize changed files and behavior, list verification commands/results, mention any remaining manual action, and never replace the user-facing report with a compression/housekeeping note. Close that report todo immediately before sending the report.",
|
|
149
149
|
);
|
|
150
150
|
const createdIds = new Set(op.kind === "create" ? [op.taskId] : op.ids);
|
|
151
151
|
const hasOlderUnfinished = !op.replacedCount && state.tasks.some((task) => {
|
|
@@ -167,7 +167,7 @@ function appendWorkflowReminder(text: string, op: Op, state: TaskState): string
|
|
|
167
167
|
}
|
|
168
168
|
if (hasInProgress) {
|
|
169
169
|
lines.push(
|
|
170
|
-
"Reminder: before your final response, update any finished todo items to completed.
|
|
170
|
+
"Reminder: before your final response, update any finished todo items to completed. If one todo is the final user-facing report step, mark it completed immediately before sending the report.",
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
return lines.join("\n\n");
|
|
@@ -250,10 +250,10 @@ export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
|
250
250
|
name: "todo",
|
|
251
251
|
label: "Todo",
|
|
252
252
|
description: "Track and keep in sync non-trivial multi-step work as todos. Actions: create, update, batch_create, batch_update, list, get, delete, clear, export, import. Supports parent/subtask hierarchy, blockers, deferred out-of-scope items, dependencies, and replace:true on create/batch_create/import for intentionally replacing an obsolete plan; skip trivial or chat-only requests. Resynchronize the plan when requirements are added, canceled, or become obsolete, whether from user input or discovered facts. For multi-step plans, include a final user-facing report todo in the initial create/batch_create plan when possible. Keep exactly one task in_progress and complete it only after verification.",
|
|
253
|
-
promptSnippet: "Track/sync non-trivial multi-step work; include final report item; resync when requirements change; keep one task in_progress",
|
|
253
|
+
promptSnippet: "Track/sync non-trivial multi-step work; include final report item and close it before sending the report; resync when requirements change; keep one task in_progress",
|
|
254
254
|
promptGuidelines: [
|
|
255
255
|
"Use `todo` for complex work with 3+ steps, explicit user task lists, or new non-trivial requirements. Skip single trivial tasks and purely conversational requests.",
|
|
256
|
-
"For any multi-step implementation/debugging plan, include a final todo item in the initial create/batch_create plan for the user-facing final report.
|
|
256
|
+
"For any multi-step implementation/debugging plan, include a final todo item in the initial create/batch_create plan for the user-facing final report. Give it explicit description/acceptance criteria covering changed files/behavior, verification commands/results, remaining manual actions, and never substitute a compression/housekeeping note for the final report. Close that report todo immediately before sending the final report to the user.",
|
|
257
257
|
"When the user adds, removes, cancels, reprioritizes, or changes the goal, scope, requirements, constraints, or chosen approach, use `todo` before continuing to synchronize the plan: update still-relevant tasks, defer or delete obsolete tasks, add new required tasks, and adjust dependencies/order.",
|
|
258
258
|
"When your own investigation or verification discovers new facts that make the current todo plan stale, incomplete, impossible, unsafe, or no longer the best approach, use `todo` to revise the plan immediately instead of following outdated tasks.",
|
|
259
259
|
"Update todos as part of the workflow, not as end-of-task cleanup: whenever you start, finish, block, split, abandon, or materially change a step, call `todo` immediately before continuing.",
|
|
@@ -52,6 +52,7 @@ type PiAntigravityCredential = {
|
|
|
52
52
|
type?: string;
|
|
53
53
|
refresh?: string;
|
|
54
54
|
email?: string;
|
|
55
|
+
activeIndex?: number;
|
|
55
56
|
clientId?: string;
|
|
56
57
|
clientSecret?: string;
|
|
57
58
|
googleClientId?: string;
|
|
@@ -62,6 +63,8 @@ type PiAntigravityCredential = {
|
|
|
62
63
|
|
|
63
64
|
type GoogleOAuthClientCredentials = { clientId: string; clientSecret?: string };
|
|
64
65
|
|
|
66
|
+
type PreparedAccount = AntigravityAccount & { originalIndex: number };
|
|
67
|
+
|
|
65
68
|
// ============================================================================
|
|
66
69
|
// 常量
|
|
67
70
|
// ============================================================================
|
|
@@ -149,10 +152,10 @@ async function readAntigravityAccounts(): Promise<AntigravityAccount[]> {
|
|
|
149
152
|
|
|
150
153
|
if (!credential) return [];
|
|
151
154
|
const credentialClient = getGoogleOAuthClientCredentials(credential);
|
|
152
|
-
const accounts = Array.isArray(credential.accounts)
|
|
155
|
+
const accounts: PreparedAccount[] = Array.isArray(credential.accounts)
|
|
153
156
|
? credential.accounts
|
|
154
157
|
.filter((account) => getAccountRefreshToken(account))
|
|
155
|
-
.map((account) => ({ ...credentialClient, ...account }))
|
|
158
|
+
.map((account, originalIndex) => ({ ...credentialClient, ...account, originalIndex }))
|
|
156
159
|
: [];
|
|
157
160
|
const primaryAccount =
|
|
158
161
|
credential.type === "oauth" && credential.refresh
|
|
@@ -161,16 +164,48 @@ async function readAntigravityAccounts(): Promise<AntigravityAccount[]> {
|
|
|
161
164
|
if (primaryAccount) {
|
|
162
165
|
primaryAccount.email = credential.email;
|
|
163
166
|
Object.assign(primaryAccount, credentialClient);
|
|
164
|
-
accounts.
|
|
167
|
+
const matchIndex = accounts.findIndex((account) =>
|
|
168
|
+
(primaryAccount.email && account.email === primaryAccount.email)
|
|
169
|
+
|| (primaryAccount.refreshToken && getAccountRefreshToken(account) === primaryAccount.refreshToken),
|
|
170
|
+
);
|
|
171
|
+
if (matchIndex >= 0) {
|
|
172
|
+
accounts[matchIndex] = { ...accounts[matchIndex], ...primaryAccount };
|
|
173
|
+
} else {
|
|
174
|
+
accounts.unshift({ ...primaryAccount, originalIndex: -1 });
|
|
175
|
+
}
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
const seen = new Set<string>();
|
|
168
|
-
|
|
179
|
+
const deduped = accounts.filter((account) => {
|
|
169
180
|
const key = account.email || account.refreshToken;
|
|
170
181
|
if (!key || seen.has(key)) return false;
|
|
171
182
|
seen.add(key);
|
|
172
183
|
return true;
|
|
173
184
|
});
|
|
185
|
+
|
|
186
|
+
const fallbackActiveIndex = deduped.findIndex((account) =>
|
|
187
|
+
(credential.email && account.email === credential.email)
|
|
188
|
+
|| (primaryAccount?.refreshToken && getAccountRefreshToken(account) === primaryAccount.refreshToken),
|
|
189
|
+
);
|
|
190
|
+
const activeIndex = Number.isInteger(credential.activeIndex)
|
|
191
|
+
? credential.activeIndex as number
|
|
192
|
+
: fallbackActiveIndex;
|
|
193
|
+
const mostRecentLastUsed = deduped.reduce(
|
|
194
|
+
(best, account, index) => (account.lastUsed > best.lastUsed ? { index, lastUsed: account.lastUsed } : best),
|
|
195
|
+
{ index: -1, lastUsed: 0 },
|
|
196
|
+
).index;
|
|
197
|
+
const priorityIndex = mostRecentLastUsed >= 0 ? mostRecentLastUsed : activeIndex;
|
|
198
|
+
|
|
199
|
+
return deduped
|
|
200
|
+
.map((account, index) => ({ account, index }))
|
|
201
|
+
.sort((a, b) => {
|
|
202
|
+
const aPriority = a.index === priorityIndex ? 1 : 0;
|
|
203
|
+
const bPriority = b.index === priorityIndex ? 1 : 0;
|
|
204
|
+
if (aPriority !== bPriority) return bPriority - aPriority;
|
|
205
|
+
if (a.account.lastUsed !== b.account.lastUsed) return b.account.lastUsed - a.account.lastUsed;
|
|
206
|
+
return a.account.originalIndex - b.account.originalIndex;
|
|
207
|
+
})
|
|
208
|
+
.map(({ account }) => account);
|
|
174
209
|
} catch (error) {
|
|
175
210
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
176
211
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,12 +61,9 @@
|
|
|
61
61
|
"prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@earendil-works/pi-
|
|
65
|
-
"@earendil-works/pi-
|
|
66
|
-
"@earendil-works/pi-
|
|
67
|
-
"@mariozechner/pi-ai": "npm:@earendil-works/pi-ai@0.79.1",
|
|
68
|
-
"@mariozechner/pi-coding-agent": "npm:@earendil-works/pi-coding-agent@0.79.1",
|
|
69
|
-
"@mariozechner/pi-tui": "npm:@earendil-works/pi-tui@0.79.1",
|
|
64
|
+
"@earendil-works/pi-ai": "0.79.3",
|
|
65
|
+
"@earendil-works/pi-coding-agent": "0.79.3",
|
|
66
|
+
"@earendil-works/pi-tui": "0.79.3",
|
|
70
67
|
"@mariozechner/clipboard": "^0.3.9",
|
|
71
68
|
"jsonc-parser": "3.3.1",
|
|
72
69
|
"typebox": "1.1.38",
|