pi-ui-extend 0.1.29 → 0.1.32
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 +3 -0
- package/dist/app/model/model-usage-status.js +134 -31
- 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/config.ts +8 -8
- package/external/pi-tools-suite/src/dcp/index.ts +1 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +5 -0
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +2 -2
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +2 -2
- 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: {
|
|
@@ -8,6 +8,7 @@ export type ModelUsageDescriptor = BaseModelUsageDescriptor & ({
|
|
|
8
8
|
readonly kind: "google-antigravity";
|
|
9
9
|
readonly quotaModelKey: string;
|
|
10
10
|
readonly account: AntigravityQuotaAccount;
|
|
11
|
+
readonly accounts?: readonly AntigravityQuotaAccount[];
|
|
11
12
|
});
|
|
12
13
|
export type ModelUsageLimitWindow = {
|
|
13
14
|
readonly remainingPercent: number;
|
|
@@ -89,6 +90,8 @@ type AntigravityQuotaAccount = {
|
|
|
89
90
|
readonly email?: string;
|
|
90
91
|
readonly refreshToken: string;
|
|
91
92
|
readonly accessToken?: string;
|
|
93
|
+
readonly clientId?: string;
|
|
94
|
+
readonly clientSecret?: string;
|
|
92
95
|
readonly cachedQuota?: AntigravityCachedQuota;
|
|
93
96
|
readonly cachedQuotaUpdatedAt?: number;
|
|
94
97
|
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;
|
|
@@ -33,14 +35,16 @@ export function modelUsageDescriptor(model) {
|
|
|
33
35
|
}
|
|
34
36
|
if (ANTIGRAVITY_QUOTA_PROVIDERS.has(provider)) {
|
|
35
37
|
const quotaModelKey = resolveAntigravityQuotaModelKey(model);
|
|
36
|
-
const
|
|
38
|
+
const accounts = readAllAntigravityQuotaAccounts();
|
|
39
|
+
const account = readActiveAntigravityQuotaAccount(accounts);
|
|
37
40
|
if (!quotaModelKey || !account)
|
|
38
41
|
return undefined;
|
|
39
42
|
return {
|
|
40
43
|
kind: "google-antigravity",
|
|
41
|
-
modelKey: `${model.provider}/${model.id}
|
|
44
|
+
modelKey: `${model.provider}/${model.id}@all:${accounts.map((item) => item.cacheKey).join(",")}`,
|
|
42
45
|
quotaModelKey,
|
|
43
46
|
account,
|
|
47
|
+
accounts,
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
return undefined;
|
|
@@ -382,15 +386,9 @@ export function resolveAntigravityQuotaModelKey(model) {
|
|
|
382
386
|
return undefined;
|
|
383
387
|
}
|
|
384
388
|
export function googleAntigravityUsageStatusFromResponse(data, descriptor, now = Date.now()) {
|
|
385
|
-
const
|
|
386
|
-
if (!
|
|
389
|
+
const window = googleAntigravityWindowFromResponse(data, descriptor.quotaModelKey, now);
|
|
390
|
+
if (!window)
|
|
387
391
|
return undefined;
|
|
388
|
-
const resetAt = parseResetTime(quotaInfo.resetTime, now);
|
|
389
|
-
const window = {
|
|
390
|
-
remainingPercent: clampPercent(Math.round((quotaInfo.remainingFraction ?? 0) * 100)),
|
|
391
|
-
resetAt,
|
|
392
|
-
windowSeconds: Math.max(0, Math.round((resetAt - now) / 1000)),
|
|
393
|
-
};
|
|
394
392
|
const weekly = window.windowSeconds >= DAY_SECONDS ? window : undefined;
|
|
395
393
|
const hourly = weekly ? undefined : window;
|
|
396
394
|
return {
|
|
@@ -402,15 +400,48 @@ export function googleAntigravityUsageStatusFromResponse(data, descriptor, now =
|
|
|
402
400
|
...(hourly ? { hourly } : {}),
|
|
403
401
|
};
|
|
404
402
|
}
|
|
403
|
+
function googleAntigravityWindowFromResponse(data, quotaModelKey, now) {
|
|
404
|
+
const quotaInfo = data.models[quotaModelKey]?.quotaInfo;
|
|
405
|
+
if (!quotaInfo)
|
|
406
|
+
return undefined;
|
|
407
|
+
const resetAt = parseResetTime(quotaInfo.resetTime, now);
|
|
408
|
+
return {
|
|
409
|
+
remainingPercent: quotaRemainingPercent(quotaInfo),
|
|
410
|
+
resetAt,
|
|
411
|
+
windowSeconds: Math.max(0, Math.round((resetAt - now) / 1000)),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
405
414
|
async function queryGoogleAntigravityModelUsage(descriptor) {
|
|
406
415
|
const now = Date.now();
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
416
|
+
const accounts = descriptor.accounts?.length ? descriptor.accounts : [descriptor.account];
|
|
417
|
+
const windows = (await Promise.all(accounts.map(async (account) => {
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetchGoogleAntigravityQuotaForAccount(account, now);
|
|
420
|
+
return googleAntigravityWindowFromResponse(response, descriptor.quotaModelKey, now);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
}))).filter((window) => window !== undefined);
|
|
426
|
+
if (windows.length === 0)
|
|
411
427
|
return undefined;
|
|
412
|
-
const
|
|
413
|
-
|
|
428
|
+
const resetAt = Math.min(...windows.map((window) => window.resetAt));
|
|
429
|
+
const windowSeconds = Math.max(0, Math.round((resetAt - now) / 1000));
|
|
430
|
+
const aggregateWindow = {
|
|
431
|
+
remainingPercent: clampPercent(Math.round(windows.reduce((sum, window) => sum + window.remainingPercent, 0) / windows.length)),
|
|
432
|
+
resetAt,
|
|
433
|
+
windowSeconds,
|
|
434
|
+
};
|
|
435
|
+
const weekly = aggregateWindow.windowSeconds >= DAY_SECONDS ? aggregateWindow : undefined;
|
|
436
|
+
const hourly = weekly ? undefined : aggregateWindow;
|
|
437
|
+
return {
|
|
438
|
+
modelKey: descriptor.modelKey,
|
|
439
|
+
provider: "google-antigravity",
|
|
440
|
+
updatedAt: now,
|
|
441
|
+
accountEmail: "Σ",
|
|
442
|
+
...(weekly ? { weekly } : {}),
|
|
443
|
+
...(hourly ? { hourly } : {}),
|
|
444
|
+
};
|
|
414
445
|
}
|
|
415
446
|
const GOOGLE_ACCOUNT_QUOTA_WINDOWS = [
|
|
416
447
|
{ label: "Claude Opus", quotaModelKey: "claude-opus-4-6-thinking" },
|
|
@@ -424,12 +455,8 @@ async function queryGoogleAntigravityAccountUsage(now) {
|
|
|
424
455
|
const results = await Promise.all(accounts.map(async (account) => {
|
|
425
456
|
const accountLabel = account.email ?? maskCredential(account.refreshToken);
|
|
426
457
|
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
|
-
}
|
|
458
|
+
const response = await fetchGoogleAntigravityQuotaForAccount(account, now);
|
|
459
|
+
const windows = googleAccountWindowsFromResponse(response, now);
|
|
433
460
|
return {
|
|
434
461
|
account: accountLabel,
|
|
435
462
|
windows,
|
|
@@ -447,8 +474,7 @@ async function queryGoogleAntigravityAccountUsage(now) {
|
|
|
447
474
|
}));
|
|
448
475
|
return results;
|
|
449
476
|
}
|
|
450
|
-
function readActiveAntigravityQuotaAccount() {
|
|
451
|
-
const accounts = readAllAntigravityQuotaAccounts();
|
|
477
|
+
function readActiveAntigravityQuotaAccount(accounts = readAllAntigravityQuotaAccounts()) {
|
|
452
478
|
const credential = readPiAuthSync().antigravity;
|
|
453
479
|
return accounts[clampAccountIndex(credential?.activeIndex, accounts.length)];
|
|
454
480
|
}
|
|
@@ -456,12 +482,14 @@ function readAllAntigravityQuotaAccounts() {
|
|
|
456
482
|
const credential = readPiAuthSync().antigravity;
|
|
457
483
|
if (!credential)
|
|
458
484
|
return [];
|
|
485
|
+
const credentialClient = getGoogleOAuthClientCredentials(credential);
|
|
459
486
|
const accounts = storedAntigravityAccounts(credential);
|
|
460
487
|
if (accounts.length > 0) {
|
|
461
488
|
const activeIndex = clampAccountIndex(credential.activeIndex, accounts.length);
|
|
462
489
|
const activeAccess = antigravityAccessFromCredential(credential);
|
|
463
490
|
return accounts.map((account, accountIndex) => antigravityQuotaAccount(account, {
|
|
464
491
|
...(credential.email ? { fallbackEmail: credential.email } : {}),
|
|
492
|
+
...(credentialClient ? { clientCredentials: credentialClient } : {}),
|
|
465
493
|
...(accountIndex === activeIndex && activeAccess ? { accessToken: activeAccess.accessToken } : {}),
|
|
466
494
|
accountIndex,
|
|
467
495
|
accountCount: accounts.length,
|
|
@@ -471,6 +499,7 @@ function readAllAntigravityQuotaAccounts() {
|
|
|
471
499
|
const fallbackAccess = antigravityAccessFromCredential(credential);
|
|
472
500
|
const account = fallbackAccount ? antigravityQuotaAccount(fallbackAccount, {
|
|
473
501
|
...(credential.email ? { fallbackEmail: credential.email } : {}),
|
|
502
|
+
...(credentialClient ? { clientCredentials: credentialClient } : {}),
|
|
474
503
|
...(fallbackAccess ? { accessToken: fallbackAccess.accessToken } : {}),
|
|
475
504
|
}) : undefined;
|
|
476
505
|
return account ? [account] : [];
|
|
@@ -483,9 +512,41 @@ function readPiAuthSync() {
|
|
|
483
512
|
return {};
|
|
484
513
|
}
|
|
485
514
|
}
|
|
515
|
+
function getAccountRefreshToken(account) {
|
|
516
|
+
if (account.refreshToken)
|
|
517
|
+
return account.refreshToken;
|
|
518
|
+
if (!account.refresh)
|
|
519
|
+
return undefined;
|
|
520
|
+
return splitAntigravityRefresh(account.refresh).refreshToken;
|
|
521
|
+
}
|
|
522
|
+
function stringProperty(source, keys) {
|
|
523
|
+
if (!source || typeof source !== "object")
|
|
524
|
+
return undefined;
|
|
525
|
+
const record = source;
|
|
526
|
+
for (const key of keys) {
|
|
527
|
+
const value = record[key];
|
|
528
|
+
if (typeof value === "string" && value)
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
function getGoogleOAuthClientCredentials(...sources) {
|
|
534
|
+
for (const source of sources) {
|
|
535
|
+
const nested = source && typeof source === "object"
|
|
536
|
+
? source.oauthClient
|
|
537
|
+
: undefined;
|
|
538
|
+
const nestedClientId = stringProperty(nested, ["clientId", "client_id", "id"]);
|
|
539
|
+
const nestedClientSecret = stringProperty(nested, ["clientSecret", "client_secret", "secret"]);
|
|
540
|
+
const clientId = nestedClientId ?? stringProperty(source, ["clientId", "client_id", "googleClientId", "google_client_id", "oauthClientId", "oauth_client_id"]);
|
|
541
|
+
const clientSecret = nestedClientSecret ?? stringProperty(source, ["clientSecret", "client_secret", "googleClientSecret", "google_client_secret", "oauthClientSecret", "oauth_client_secret"]);
|
|
542
|
+
if (clientId)
|
|
543
|
+
return { clientId, ...(clientSecret ? { clientSecret } : {}) };
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
486
547
|
function storedAntigravityAccounts(credential) {
|
|
487
548
|
return Array.isArray(credential.accounts)
|
|
488
|
-
? credential.accounts.filter((account) => account.enabled !== false && !!account
|
|
549
|
+
? credential.accounts.filter((account) => account.enabled !== false && !!getAccountRefreshToken(account))
|
|
489
550
|
: [];
|
|
490
551
|
}
|
|
491
552
|
function antigravityAccountFromCredential(credential) {
|
|
@@ -506,16 +567,19 @@ function antigravityAccountFromCredential(credential) {
|
|
|
506
567
|
};
|
|
507
568
|
}
|
|
508
569
|
function antigravityQuotaAccount(account, options = {}) {
|
|
509
|
-
const refreshToken = account
|
|
570
|
+
const refreshToken = getAccountRefreshToken(account);
|
|
510
571
|
if (!refreshToken)
|
|
511
572
|
return undefined;
|
|
512
573
|
const email = account.email || options.fallbackEmail;
|
|
513
574
|
const projectId = account.projectId || account.managedProjectId || DEFAULT_ANTIGRAVITY_PROJECT_ID;
|
|
575
|
+
const clientCredentials = getGoogleOAuthClientCredentials(account, options.clientCredentials);
|
|
514
576
|
return {
|
|
515
577
|
refreshToken,
|
|
516
578
|
projectId,
|
|
517
579
|
cacheKey: email ? email.toLowerCase() : shortHash(refreshToken),
|
|
518
580
|
...(options.accessToken ? { accessToken: options.accessToken } : {}),
|
|
581
|
+
...(clientCredentials ? { clientId: clientCredentials.clientId } : {}),
|
|
582
|
+
...(clientCredentials?.clientSecret ? { clientSecret: clientCredentials.clientSecret } : {}),
|
|
519
583
|
...(account.cachedQuota ? { cachedQuota: account.cachedQuota } : {}),
|
|
520
584
|
...(typeof account.cachedQuotaUpdatedAt === "number" ? { cachedQuotaUpdatedAt: account.cachedQuotaUpdatedAt } : {}),
|
|
521
585
|
...(email ? { email } : {}),
|
|
@@ -533,9 +597,9 @@ function googleQuotaResponseFromCachedQuota(cachedQuota, cachedQuotaUpdatedAt, n
|
|
|
533
597
|
return Object.keys(models).length > 0 ? { models } : undefined;
|
|
534
598
|
}
|
|
535
599
|
function addCachedQuotaModels(models, quota, quotaModelKeys, cachedQuotaUpdatedAt, now) {
|
|
536
|
-
if (!quota
|
|
600
|
+
if (!quota)
|
|
537
601
|
return;
|
|
538
|
-
const remainingFraction = quota.remainingFraction;
|
|
602
|
+
const remainingFraction = Number.isFinite(quota.remainingFraction) ? quota.remainingFraction : 0;
|
|
539
603
|
const resetTime = cachedQuotaResetTimeForDisplay(quota.resetTime, cachedQuotaUpdatedAt, now);
|
|
540
604
|
for (const quotaModelKey of quotaModelKeys) {
|
|
541
605
|
models[quotaModelKey] = {
|
|
@@ -590,13 +654,49 @@ function clampAccountIndex(index, accountCount) {
|
|
|
590
654
|
function shortHash(value) {
|
|
591
655
|
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
592
656
|
}
|
|
657
|
+
async function fetchGoogleAntigravityQuotaForAccount(account, now = Date.now()) {
|
|
658
|
+
if (account.accessToken)
|
|
659
|
+
return await fetchGoogleAntigravityQuota(account.accessToken, account.projectId);
|
|
660
|
+
if (account.clientId) {
|
|
661
|
+
const { accessToken } = await refreshGoogleAntigravityAccessToken(account);
|
|
662
|
+
return await fetchGoogleAntigravityQuota(accessToken, account.projectId);
|
|
663
|
+
}
|
|
664
|
+
const cachedResponse = googleQuotaResponseFromCachedQuota(account.cachedQuota, account.cachedQuotaUpdatedAt, now);
|
|
665
|
+
if (cachedResponse)
|
|
666
|
+
return cachedResponse;
|
|
667
|
+
throw new Error("Missing Google OAuth client credentials, cannot query live Antigravity quota.");
|
|
668
|
+
}
|
|
669
|
+
async function refreshGoogleAntigravityAccessToken(account) {
|
|
670
|
+
if (!account.clientId)
|
|
671
|
+
throw new Error("Missing Google OAuth client id, cannot refresh Antigravity access token.");
|
|
672
|
+
const params = new URLSearchParams({
|
|
673
|
+
client_id: account.clientId,
|
|
674
|
+
refresh_token: account.refreshToken,
|
|
675
|
+
grant_type: "refresh_token",
|
|
676
|
+
});
|
|
677
|
+
if (account.clientSecret)
|
|
678
|
+
params.set("client_secret", account.clientSecret);
|
|
679
|
+
const response = await fetchWithTimeout(GOOGLE_TOKEN_REFRESH_URL, {
|
|
680
|
+
method: "POST",
|
|
681
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
682
|
+
body: params,
|
|
683
|
+
});
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
const errorText = await response.text();
|
|
686
|
+
throw new Error(`Google token refresh failed (${response.status}): ${errorText}`);
|
|
687
|
+
}
|
|
688
|
+
const data = await response.json();
|
|
689
|
+
if (!data.access_token)
|
|
690
|
+
throw new Error("Google token refresh did not return an access token.");
|
|
691
|
+
return { accessToken: data.access_token };
|
|
692
|
+
}
|
|
593
693
|
async function fetchGoogleAntigravityQuota(accessToken, projectId) {
|
|
594
694
|
const response = await fetchWithTimeout(GOOGLE_QUOTA_API_URL, {
|
|
595
695
|
method: "POST",
|
|
596
696
|
headers: {
|
|
597
697
|
"Content-Type": "application/json",
|
|
598
698
|
Authorization: `Bearer ${accessToken}`,
|
|
599
|
-
"User-Agent":
|
|
699
|
+
"User-Agent": GOOGLE_ANTIGRAVITY_USER_AGENT,
|
|
600
700
|
},
|
|
601
701
|
body: JSON.stringify({ project: projectId }),
|
|
602
702
|
});
|
|
@@ -757,16 +857,19 @@ function accountWindowFromRateLimit(window, now) {
|
|
|
757
857
|
}
|
|
758
858
|
function googleAccountWindowFromResponse(data, label, quotaModelKey, now) {
|
|
759
859
|
const quotaInfo = data.models[quotaModelKey]?.quotaInfo;
|
|
760
|
-
if (!quotaInfo
|
|
860
|
+
if (!quotaInfo)
|
|
761
861
|
return undefined;
|
|
762
862
|
const resetAt = parseResetTime(quotaInfo.resetTime, now);
|
|
763
863
|
return {
|
|
764
864
|
label,
|
|
765
|
-
remainingPercent:
|
|
865
|
+
remainingPercent: quotaRemainingPercent(quotaInfo),
|
|
766
866
|
resetAt,
|
|
767
867
|
windowSeconds: Math.max(0, Math.round((resetAt - now) / 1000)),
|
|
768
868
|
};
|
|
769
869
|
}
|
|
870
|
+
function quotaRemainingPercent(quotaInfo) {
|
|
871
|
+
return clampPercent(Math.round((Number.isFinite(quotaInfo.remainingFraction) ? quotaInfo.remainingFraction : 0) * 100));
|
|
872
|
+
}
|
|
770
873
|
function googleAccountWindowsFromResponse(data, now) {
|
|
771
874
|
return GOOGLE_ACCOUNT_QUOTA_WINDOWS
|
|
772
875
|
.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"
|
|
@@ -24,8 +24,8 @@ export interface DcpConfig {
|
|
|
24
24
|
modelMaxContextLimits?: Record<string, number | string>
|
|
25
25
|
modelMinContextLimits?: Record<string, number | string>
|
|
26
26
|
summaryBuffer: boolean
|
|
27
|
-
nudgeFrequency: number // inject nudge every N context events (default:
|
|
28
|
-
iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default:
|
|
27
|
+
nudgeFrequency: number // inject nudge every N context events (default: 2)
|
|
28
|
+
iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default: 8)
|
|
29
29
|
nudgeForce: "strong" | "soft"
|
|
30
30
|
protectedTools: string[] // these tool outputs always protected from pruning
|
|
31
31
|
protectTags: boolean
|
|
@@ -81,27 +81,27 @@ const DEFAULT_CONFIG: DcpConfig = {
|
|
|
81
81
|
automaticStrategies: true,
|
|
82
82
|
},
|
|
83
83
|
compress: {
|
|
84
|
-
maxContextPercent: 0.
|
|
85
|
-
minContextPercent: 0.
|
|
84
|
+
maxContextPercent: 0.65,
|
|
85
|
+
minContextPercent: 0.25,
|
|
86
86
|
modelMaxContextPercent: {},
|
|
87
87
|
modelMinContextPercent: {},
|
|
88
88
|
summaryBuffer: true,
|
|
89
|
-
nudgeFrequency:
|
|
90
|
-
iterationNudgeThreshold:
|
|
89
|
+
nudgeFrequency: 2,
|
|
90
|
+
iterationNudgeThreshold: 8,
|
|
91
91
|
nudgeForce: "soft",
|
|
92
92
|
protectedTools: ["compress", "write", "edit"],
|
|
93
93
|
protectTags: false,
|
|
94
94
|
protectUserMessages: false,
|
|
95
95
|
autoCandidates: {
|
|
96
96
|
enabled: true,
|
|
97
|
-
minContextPercent: 0.
|
|
97
|
+
minContextPercent: 0.25,
|
|
98
98
|
keepRecentTurns: 2,
|
|
99
99
|
minMessages: 6,
|
|
100
100
|
minTokens: 1500,
|
|
101
101
|
},
|
|
102
102
|
messageMode: {
|
|
103
103
|
enabled: true,
|
|
104
|
-
minContextPercent: 0.
|
|
104
|
+
minContextPercent: 0.25,
|
|
105
105
|
keepRecentTurns: 2,
|
|
106
106
|
mediumTokens: 500,
|
|
107
107
|
highTokens: 5000,
|
|
@@ -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,
|
|
@@ -174,6 +174,7 @@ You are at or beyond the configured max context threshold. This is an emergency
|
|
|
174
174
|
You MUST use the \`compress\` tool now. Do not continue normal exploration until compression is handled.
|
|
175
175
|
|
|
176
176
|
If you are in the middle of a critical atomic operation, finish that atomic step first, then compress immediately.
|
|
177
|
+
If a completed implementation+verification slice exists, compress it before replying or starting another task.
|
|
177
178
|
|
|
178
179
|
RANGE STRATEGY (MANDATORY)
|
|
179
180
|
Prioritize one large, closed, high-yield compression range first.
|
|
@@ -201,6 +202,8 @@ ACTION REQUIRED: Context usage is high.
|
|
|
201
202
|
Before doing more exploration, look for a closed, self-contained range that no longer needs to stay raw and compress it now.
|
|
202
203
|
|
|
203
204
|
Do not treat this as optional housekeeping. If any completed research, implementation, verification, CI-log inspection, or dead-end debugging slice is present, call the \`compress\` tool before continuing normal work.
|
|
205
|
+
If a completed implementation+verification slice exists, compress it before replying or starting another task.
|
|
206
|
+
High-priority stale tool outputs must be compressed once no exact raw text is needed.
|
|
204
207
|
|
|
205
208
|
RANGE SELECTION
|
|
206
209
|
Prefer older, resolved history. Avoid the newest active working slice unless it is clearly done.
|
|
@@ -222,6 +225,7 @@ If any range is cleanly closed and unlikely to be needed again, use the \`compre
|
|
|
222
225
|
If direction has shifted, compress earlier ranges that are now less relevant.
|
|
223
226
|
|
|
224
227
|
Do not defer this across another batch of searches, reads, CI log fetches, or tests. The next safe action should be compression whenever a closed slice exists.
|
|
228
|
+
High-priority stale tool outputs must be compressed once no exact raw text is needed.
|
|
225
229
|
|
|
226
230
|
Prefer small, closed-range compressions over one broad compression.
|
|
227
231
|
Use message-mode compression for isolated large stale messages.
|
|
@@ -238,6 +242,7 @@ ACTION REQUIRED: You've been iterating for a while after the last user message.
|
|
|
238
242
|
Pause before the next non-atomic tool call. If there is a closed portion that is unlikely to be referenced immediately (for example, finished research before implementation, completed CI-log triage, a verified fix, or a dead-end investigation), use the \`compress\` tool on it now.
|
|
239
243
|
|
|
240
244
|
Do not keep accumulating tool outputs while a completed slice remains raw. If a range is closed, compression is the next safe action.
|
|
245
|
+
If a completed implementation+verification slice exists, compress it before replying or starting another task.
|
|
241
246
|
|
|
242
247
|
Prefer multiple short, closed ranges over one large range when several independent slices are ready.
|
|
243
248
|
Use message-mode compression for isolated large stale messages.
|
|
@@ -149,11 +149,11 @@ export function resolveContextThresholds(
|
|
|
149
149
|
minContextPercent: min ??
|
|
150
150
|
resolveThresholdValue(config.compress.minContextLimit) ??
|
|
151
151
|
resolveThresholdValue(config.compress.minContextPercent) ??
|
|
152
|
-
0.
|
|
152
|
+
0.25,
|
|
153
153
|
maxContextPercent: max ??
|
|
154
154
|
resolveThresholdValue(config.compress.maxContextLimit) ??
|
|
155
155
|
resolveThresholdValue(config.compress.maxContextPercent) ??
|
|
156
|
-
0.
|
|
156
|
+
0.65,
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -291,11 +291,11 @@ export function getNudgeType(
|
|
|
291
291
|
config.compress;
|
|
292
292
|
const minContextPercent = coercePercentThreshold(
|
|
293
293
|
thresholds.minContextPercent ?? config.compress.minContextPercent,
|
|
294
|
-
0.
|
|
294
|
+
0.25,
|
|
295
295
|
);
|
|
296
296
|
const maxContextPercent = coercePercentThreshold(
|
|
297
297
|
thresholds.maxContextPercent ?? config.compress.maxContextPercent,
|
|
298
|
-
0.
|
|
298
|
+
0.65,
|
|
299
299
|
);
|
|
300
300
|
const cadence = Math.max(1, Math.floor(nudgeFrequency));
|
|
301
301
|
|
|
@@ -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.32",
|
|
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",
|