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.
Files changed (36) hide show
  1. package/dist/app/app.d.ts +2 -0
  2. package/dist/app/app.js +31 -8
  3. package/dist/app/cli/update.d.ts +5 -0
  4. package/dist/app/cli/update.js +29 -1
  5. package/dist/app/model/model-usage-status.d.ts +3 -0
  6. package/dist/app/model/model-usage-status.js +134 -31
  7. package/dist/app/session/session-event-controller.d.ts +17 -1
  8. package/dist/app/session/session-event-controller.js +28 -0
  9. package/dist/app/session/tabs-controller.d.ts +10 -0
  10. package/dist/app/session/tabs-controller.js +65 -28
  11. package/external/pi-tools-suite/package.json +0 -3
  12. package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
  13. package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
  14. package/external/pi-tools-suite/src/async-subagents/index.ts +1 -1
  15. package/external/pi-tools-suite/src/async-subagents/render.ts +1 -1
  16. package/external/pi-tools-suite/src/async-subagents/tools/cleanup.ts +2 -2
  17. package/external/pi-tools-suite/src/async-subagents/tools/result.ts +2 -2
  18. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +3 -3
  19. package/external/pi-tools-suite/src/async-subagents/tools/status.ts +3 -3
  20. package/external/pi-tools-suite/src/async-subagents/tools/stop.ts +2 -2
  21. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -3
  22. package/external/pi-tools-suite/src/async-subagents/tools/wait.ts +2 -2
  23. package/external/pi-tools-suite/src/async-subagents/ui.ts +1 -1
  24. package/external/pi-tools-suite/src/dcp/commands.ts +2 -2
  25. package/external/pi-tools-suite/src/dcp/compress-tool.ts +1 -1
  26. package/external/pi-tools-suite/src/dcp/config.ts +8 -8
  27. package/external/pi-tools-suite/src/dcp/index.ts +1 -1
  28. package/external/pi-tools-suite/src/dcp/prompts.ts +5 -0
  29. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +2 -2
  30. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +2 -2
  31. package/external/pi-tools-suite/src/model-tools/apply-patch.ts +1 -1
  32. package/external/pi-tools-suite/src/model-tools/index.ts +1 -1
  33. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +2 -2
  34. package/external/pi-tools-suite/src/tool-descriptions.ts +2 -2
  35. package/external/pi-tools-suite/src/usage/lib/google.ts +39 -4
  36. 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
- void this.checkPixUpdateOnStartup();
790
+ this.checkPixUpdateOnStartup();
789
791
  }
790
- async checkPixUpdateOnStartup() {
791
- try {
792
- const result = await checkPixUpdate();
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
- catch {
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,
@@ -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>;
@@ -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() : "pi-ui-extend";
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 account = readActiveAntigravityQuotaAccount();
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}@${account.cacheKey}`,
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 quotaInfo = data.models[descriptor.quotaModelKey]?.quotaInfo;
386
- if (!quotaInfo || !Number.isFinite(quotaInfo.remainingFraction))
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 cachedResponse = googleQuotaResponseFromCachedQuota(descriptor.account.cachedQuota, descriptor.account.cachedQuotaUpdatedAt, now);
408
- if (cachedResponse)
409
- return googleAntigravityUsageStatusFromResponse(cachedResponse, descriptor, now);
410
- if (!descriptor.account.accessToken)
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 response = await fetchGoogleAntigravityQuota(descriptor.account.accessToken, descriptor.account.projectId);
413
- return googleAntigravityUsageStatusFromResponse(response, descriptor);
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 = account.cachedQuota ? googleQuotaResponseFromCachedQuota(account.cachedQuota, account.cachedQuotaUpdatedAt, now) : undefined;
428
- const windows = response ? googleAccountWindowsFromResponse(response, now) : [];
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.refreshToken)
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.refreshToken;
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 || !Number.isFinite(quota.remainingFraction))
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": "antigravity/1.18.3 darwin/arm64",
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 || !Number.isFinite(quotaInfo.remainingFraction))
860
+ if (!quotaInfo)
761
861
  return undefined;
762
862
  const resetAt = parseResetTime(quotaInfo.resetTime, now);
763
863
  return {
764
864
  label,
765
- remainingPercent: clampPercent(Math.round((quotaInfo.remainingFraction ?? 0) * 100)),
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
- await this.loadActiveSessionHistory(targetRuntime);
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, delayMs === 3000);
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, finalAttempt) {
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 (finalAttempt && tabId === this.activeTabId && this.host.runtime() === runtime) {
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 byteCount = Math.min(size, SESSION_TITLE_SCAN_MAX_BYTES);
1509
- const buffer = Buffer.alloc(byteCount);
1510
- await file.read(buffer, 0, byteCount, size - byteCount);
1511
- const text = buffer.toString("utf8");
1512
- const lines = text.split("\n");
1513
- if (size > byteCount)
1514
- lines.shift();
1515
- for (let index = lines.length - 1; index >= 0; index -= 1) {
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 type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import {
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export const SUBAGENT_DENIED_TOOLS = new Set([
4
4
  "question",
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import {
@@ -1,4 +1,4 @@
1
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
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 "@mariozechner/pi-coding-agent";
2
- import { Type } from "@mariozechner/pi-ai";
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 "@mariozechner/pi-coding-agent";
2
- import { Type } from "@mariozechner/pi-ai";
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 "@mariozechner/pi-coding-agent";
4
- import { Type } from "@mariozechner/pi-ai";
5
- import { Text } from "@mariozechner/pi-tui";
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 "@mariozechner/pi-coding-agent";
2
- import { Type } from "@mariozechner/pi-ai";
3
- import { Text } from "@mariozechner/pi-tui";
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 "@mariozechner/pi-coding-agent";
2
- import { Type } from "@mariozechner/pi-ai";
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 "@mariozechner/pi-coding-agent";
2
- import { Type } from "@mariozechner/pi-ai";
3
- import { Text } from "@mariozechner/pi-tui";
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 "@mariozechner/pi-coding-agent";
2
- import { Type } from "@mariozechner/pi-ai";
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,4 +1,4 @@
1
- import { Container } from "@mariozechner/pi-tui";
1
+ import { Container } from "@earendil-works/pi-tui";
2
2
 
3
3
  export function emptyToolSlot(): Container {
4
4
  return new Container();
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent"
2
- import type { AutocompleteItem } from "@mariozechner/pi-tui"
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 "@mariozechner/pi-coding-agent"
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: 5)
28
- iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default: 15)
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.8,
85
- minContextPercent: 0.4,
84
+ maxContextPercent: 0.65,
85
+ minContextPercent: 0.25,
86
86
  modelMaxContextPercent: {},
87
87
  modelMinContextPercent: {},
88
88
  summaryBuffer: true,
89
- nudgeFrequency: 5,
90
- iterationNudgeThreshold: 15,
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.4,
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.4,
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 "@mariozechner/pi-coding-agent"
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.4,
152
+ 0.25,
153
153
  maxContextPercent: max ??
154
154
  resolveThresholdValue(config.compress.maxContextLimit) ??
155
155
  resolveThresholdValue(config.compress.maxContextPercent) ??
156
- 0.8,
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.4,
294
+ 0.25,
295
295
  );
296
296
  const maxContextPercent = coercePercentThreshold(
297
297
  thresholds.maxContextPercent ?? config.compress.maxContextPercent,
298
- 0.8,
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 "@mariozechner/pi-coding-agent";
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 "@mariozechner/pi-coding-agent";
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. Treat the final user-facing report step like any other todo: mark it completed immediately before sending the report.",
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. Treat it like any other todo item: as soon as that step is finished, mark it completed immediately. 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.",
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.unshift(primaryAccount);
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
- return accounts.filter((account) => {
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.29",
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-tui": "0.79.1",
65
- "@earendil-works/pi-ai": "0.79.1",
66
- "@earendil-works/pi-coding-agent": "0.79.1",
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",