pi-ui-extend 0.1.9 → 0.1.13

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 (121) hide show
  1. package/README.md +23 -2
  2. package/dist/app/app.d.ts +4 -0
  3. package/dist/app/app.js +76 -7
  4. package/dist/app/cli/install.d.ts +16 -0
  5. package/dist/app/cli/install.js +34 -7
  6. package/dist/app/cli/startup-info.js +5 -2
  7. package/dist/app/cli/update.d.ts +7 -0
  8. package/dist/app/cli/update.js +11 -3
  9. package/dist/app/commands/command-controller.js +4 -0
  10. package/dist/app/commands/command-host.d.ts +4 -0
  11. package/dist/app/commands/command-model-actions.d.ts +5 -0
  12. package/dist/app/commands/command-model-actions.js +104 -0
  13. package/dist/app/commands/command-navigation-actions.d.ts +6 -1
  14. package/dist/app/commands/command-navigation-actions.js +37 -14
  15. package/dist/app/commands/command-registry.d.ts +4 -0
  16. package/dist/app/commands/command-registry.js +32 -0
  17. package/dist/app/commands/command-session-actions.d.ts +1 -0
  18. package/dist/app/commands/command-session-actions.js +15 -5
  19. package/dist/app/commands/shell-command.d.ts +7 -0
  20. package/dist/app/commands/shell-command.js +12 -4
  21. package/dist/app/commands/shell-controller.d.ts +1 -0
  22. package/dist/app/commands/shell-controller.js +1 -1
  23. package/dist/app/constants.d.ts +1 -1
  24. package/dist/app/constants.js +1 -1
  25. package/dist/app/icons.d.ts +1 -0
  26. package/dist/app/icons.js +3 -1
  27. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  28. package/dist/app/input/autocomplete-controller.js +352 -0
  29. package/dist/app/input/input-action-controller.d.ts +1 -0
  30. package/dist/app/input/input-action-controller.js +21 -0
  31. package/dist/app/input/input-controller.d.ts +1 -0
  32. package/dist/app/input/input-controller.js +2 -0
  33. package/dist/app/input/input-paste-handler.d.ts +1 -0
  34. package/dist/app/input/input-paste-handler.js +22 -18
  35. package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
  36. package/dist/app/input/prompt-enhancer-controller.js +12 -3
  37. package/dist/app/input/voice-controller.d.ts +51 -1
  38. package/dist/app/input/voice-controller.js +42 -19
  39. package/dist/app/model/model-usage-status.d.ts +9 -0
  40. package/dist/app/model/model-usage-status.js +124 -34
  41. package/dist/app/popup/popup-action-controller.js +1 -1
  42. package/dist/app/process.d.ts +17 -0
  43. package/dist/app/process.js +68 -0
  44. package/dist/app/rendering/conversation-entry-renderer.js +8 -6
  45. package/dist/app/rendering/conversation-tool-renderer.js +3 -2
  46. package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
  47. package/dist/app/rendering/editor-layout-renderer.js +11 -1
  48. package/dist/app/rendering/message-content.js +65 -7
  49. package/dist/app/rendering/render-controller.js +6 -1
  50. package/dist/app/rendering/render-text.d.ts +3 -0
  51. package/dist/app/rendering/render-text.js +51 -3
  52. package/dist/app/rendering/status-line-renderer.d.ts +5 -1
  53. package/dist/app/rendering/status-line-renderer.js +61 -25
  54. package/dist/app/rendering/toast-renderer.js +10 -13
  55. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  56. package/dist/app/rendering/tool-block-renderer.js +16 -33
  57. package/dist/app/runtime.d.ts +6 -1
  58. package/dist/app/runtime.js +35 -2
  59. package/dist/app/screen/clipboard.d.ts +11 -2
  60. package/dist/app/screen/clipboard.js +29 -21
  61. package/dist/app/screen/file-link-opener.d.ts +8 -0
  62. package/dist/app/screen/file-link-opener.js +11 -3
  63. package/dist/app/screen/file-links.js +3 -3
  64. package/dist/app/screen/image-opener.d.ts +12 -0
  65. package/dist/app/screen/image-opener.js +13 -5
  66. package/dist/app/screen/mouse-controller.d.ts +5 -2
  67. package/dist/app/screen/mouse-controller.js +16 -1
  68. package/dist/app/screen/screen-styler.d.ts +4 -1
  69. package/dist/app/screen/screen-styler.js +3 -2
  70. package/dist/app/screen/status-controller.d.ts +3 -0
  71. package/dist/app/screen/status-controller.js +23 -8
  72. package/dist/app/session/queued-message-controller.d.ts +7 -1
  73. package/dist/app/session/queued-message-controller.js +36 -21
  74. package/dist/app/session/resume-session-loader.d.ts +15 -0
  75. package/dist/app/session/resume-session-loader.js +204 -0
  76. package/dist/app/session/session-event-controller.d.ts +5 -1
  77. package/dist/app/session/session-event-controller.js +72 -5
  78. package/dist/app/session/session-history.js +4 -3
  79. package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
  80. package/dist/app/session/session-lifecycle-controller.js +9 -1
  81. package/dist/app/session/tabs-controller.d.ts +10 -1
  82. package/dist/app/session/tabs-controller.js +101 -5
  83. package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
  84. package/dist/app/terminal/nerd-font-controller.js +30 -23
  85. package/dist/app/terminal/terminal-controller.d.ts +1 -0
  86. package/dist/app/terminal/terminal-controller.js +1 -0
  87. package/dist/app/types.d.ts +14 -0
  88. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
  89. package/dist/app/workspace/workspace-actions-controller.js +3 -3
  90. package/dist/app/workspace/workspace-undo.d.ts +1 -1
  91. package/dist/app/workspace/workspace-undo.js +22 -20
  92. package/dist/config.d.ts +27 -0
  93. package/dist/config.js +174 -1
  94. package/dist/default-pix-config.js +39 -353
  95. package/dist/input-editor.d.ts +7 -1
  96. package/dist/input-editor.js +47 -6
  97. package/dist/markdown-format.d.ts +1 -0
  98. package/dist/markdown-format.js +26 -1
  99. package/dist/schemas/index.d.ts +5 -0
  100. package/dist/schemas/index.js +5 -0
  101. package/dist/schemas/pi-tools-suite-schema.d.ts +177 -0
  102. package/dist/schemas/pi-tools-suite-schema.js +218 -0
  103. package/dist/schemas/pix-schema.d.ts +65 -0
  104. package/dist/schemas/pix-schema.js +91 -0
  105. package/dist/terminal-width.js +73 -56
  106. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
  107. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  108. package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
  109. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +46 -195
  110. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  111. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  112. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  113. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  114. package/external/pi-tools-suite/src/todo/index.ts +4 -2
  115. package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
  116. package/external/pi-tools-suite/src/todo/todo.ts +2 -6
  117. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  118. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  119. package/package.json +12 -3
  120. package/schemas/pi-tools-suite.json +881 -0
  121. package/schemas/pix.json +298 -0
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { createWriteStream } from "node:fs";
3
3
  import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
4
  import http from "node:http";
@@ -8,13 +8,26 @@ import { join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { savePixDictationLanguage } from "../../config.js";
10
10
  import { APP_ICONS } from "../icons.js";
11
+ import { commandExists } from "../process.js";
11
12
  const SAMPLE_RATE = 16_000;
12
13
  const require = createRequire(import.meta.url);
13
14
  const projectRoot = fileURLToPath(new URL("../..", import.meta.url));
14
15
  const modelsRoot = join(projectRoot, "models", "vosk");
15
16
  const VOSK_PACKAGE_SPEC = "vosk@0.3.39";
16
17
  const VOICE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
18
+ const VOICE_PARTIAL_TRANSCRIPT_THROTTLE_MS = 100;
17
19
  let voskInstallPromise;
20
+ const defaultVoiceControllerDeps = {
21
+ tryLoadVosk,
22
+ ensureModel,
23
+ selectRecorderCommand,
24
+ spawn,
25
+ savePixDictationLanguage,
26
+ };
27
+ let voiceControllerDeps = defaultVoiceControllerDeps;
28
+ export function setVoiceControllerTestDeps(overrides) {
29
+ voiceControllerDeps = overrides ? { ...defaultVoiceControllerDeps, ...overrides } : defaultVoiceControllerDeps;
30
+ }
18
31
  export class AppVoiceController {
19
32
  host;
20
33
  modelDefinitions;
@@ -29,6 +42,7 @@ export class AppVoiceController {
29
42
  progressTimer;
30
43
  lastSystemProgressMessage;
31
44
  partialTranscript;
45
+ partialTranscriptTimer;
32
46
  startGeneration = 0;
33
47
  constructor(host, dictationConfig) {
34
48
  this.host = host;
@@ -113,7 +127,7 @@ export class AppVoiceController {
113
127
  const generation = this.startGeneration + 1;
114
128
  this.startGeneration = generation;
115
129
  try {
116
- const initialVosk = tryLoadVosk();
130
+ const initialVosk = voiceControllerDeps.tryLoadVosk();
117
131
  const vosk = initialVosk.ok
118
132
  ? initialVosk.module
119
133
  : await this.installAndLoadVosk(initialVosk.error, generation);
@@ -122,15 +136,15 @@ export class AppVoiceController {
122
136
  vosk.setLogLevel?.(-1);
123
137
  this.state = "downloading";
124
138
  this.host.render();
125
- const modelPath = await ensureModel(language, this.modelDefinition(language));
139
+ const modelPath = await voiceControllerDeps.ensureModel(language, this.modelDefinition(language));
126
140
  if (!this.isCurrentStart(generation))
127
141
  return;
128
142
  this.state = "loading";
129
143
  this.host.render();
130
144
  const model = this.cachedModel(language, modelPath, vosk);
131
- const recorder = selectRecorderCommand();
145
+ const recorder = await voiceControllerDeps.selectRecorderCommand();
132
146
  const recognizer = new vosk.Recognizer({ model, sampleRate: SAMPLE_RATE });
133
- const audioProcess = spawn(recorder.command, recorder.args, { stdio: ["ignore", "pipe", "pipe"] });
147
+ const audioProcess = voiceControllerDeps.spawn(recorder.command, recorder.args, { stdio: ["ignore", "pipe", "pipe"] });
134
148
  this.recognizer = recognizer;
135
149
  this.audioProcess = audioProcess;
136
150
  this.state = "listening";
@@ -169,7 +183,7 @@ export class AppVoiceController {
169
183
  }
170
184
  saveLanguageSelection(language) {
171
185
  try {
172
- savePixDictationLanguage(language);
186
+ voiceControllerDeps.savePixDictationLanguage(language);
173
187
  }
174
188
  catch (error) {
175
189
  this.host.showToast(`Could not save voice language: ${errorMessage(error)}`, "warning");
@@ -283,14 +297,27 @@ export class AppVoiceController {
283
297
  if (text === this.partialTranscript)
284
298
  return;
285
299
  this.partialTranscript = text;
286
- this.host.setPartialTranscript(text);
300
+ this.schedulePartialTranscriptEmit();
287
301
  }
288
302
  clearPartialTranscript() {
289
303
  if (!this.partialTranscript)
290
304
  return;
291
305
  this.partialTranscript = undefined;
306
+ if (this.partialTranscriptTimer) {
307
+ clearTimeout(this.partialTranscriptTimer);
308
+ this.partialTranscriptTimer = undefined;
309
+ }
292
310
  this.host.setPartialTranscript(undefined);
293
311
  }
312
+ schedulePartialTranscriptEmit() {
313
+ if (this.partialTranscriptTimer)
314
+ return;
315
+ this.partialTranscriptTimer = setTimeout(() => {
316
+ this.partialTranscriptTimer = undefined;
317
+ this.host.setPartialTranscript(this.partialTranscript);
318
+ }, VOICE_PARTIAL_TRANSCRIPT_THROTTLE_MS);
319
+ this.partialTranscriptTimer.unref?.();
320
+ }
294
321
  isCurrentStart(generation) {
295
322
  return this.startGeneration === generation;
296
323
  }
@@ -367,11 +394,11 @@ async function downloadFile(url, destination, redirects = 3) {
367
394
  });
368
395
  }
369
396
  async function extractZip(zipPath, destination) {
370
- if (commandExists("unzip")) {
397
+ if (await commandExists("unzip")) {
371
398
  await runCommand("unzip", ["-q", zipPath, "-d", destination]);
372
399
  return;
373
400
  }
374
- if (process.platform === "darwin" && commandExists("ditto")) {
401
+ if (process.platform === "darwin" && await commandExists("ditto")) {
375
402
  await runCommand("ditto", ["-x", "-k", zipPath, destination]);
376
403
  return;
377
404
  }
@@ -535,7 +562,7 @@ function isVoskModule(value) {
535
562
  const record = value;
536
563
  return typeof record.Model === "function" && typeof record.Recognizer === "function";
537
564
  }
538
- function selectRecorderCommand() {
565
+ async function selectRecorderCommand() {
539
566
  const commands = [
540
567
  {
541
568
  command: "rec",
@@ -566,15 +593,11 @@ function selectRecorderCommand() {
566
593
  description: "arecord",
567
594
  });
568
595
  }
569
- const command = commands.find((candidate) => commandExists(candidate.command));
570
- if (!command)
571
- throw new Error("audio recorder not found: install SoX (`rec`/`sox`), ffmpeg, or arecord");
572
- return command;
573
- }
574
- function commandExists(command) {
575
- if (process.platform === "win32")
576
- return spawnSync("where", [command], { stdio: "ignore" }).status === 0;
577
- return spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" }).status === 0;
596
+ for (const candidate of commands) {
597
+ if (await commandExists(candidate.command))
598
+ return candidate;
599
+ }
600
+ throw new Error("audio recorder not found: install SoX (`rec`/`sox`), ffmpeg, or arecord");
578
601
  }
579
602
  function transcriptText(result) {
580
603
  const parsed = typeof result === "string" ? parseResultString(result) : result;
@@ -79,9 +79,18 @@ export type OpenAIUsageResponse = {
79
79
  rate_limit: OpenAIRateLimit | null;
80
80
  additional_rate_limits?: OpenAIAdditionalRateLimit[];
81
81
  };
82
+ type AntigravityCachedQuotaBucket = {
83
+ remainingFraction?: number;
84
+ resetTime?: string;
85
+ modelCount?: number;
86
+ };
87
+ type AntigravityCachedQuota = Record<string, AntigravityCachedQuotaBucket | undefined>;
82
88
  type AntigravityQuotaAccount = {
83
89
  readonly email?: string;
84
90
  readonly refreshToken: string;
91
+ readonly accessToken?: string;
92
+ readonly cachedQuota?: AntigravityCachedQuota;
93
+ readonly cachedQuotaUpdatedAt?: number;
85
94
  readonly projectId: string;
86
95
  readonly accountIndex?: number;
87
96
  readonly accountCount?: number;
@@ -9,13 +9,10 @@ const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
9
9
  const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
10
10
  const ZHIPU_QUOTA_URL = "https://bigmodel.cn/api/monitor/usage/quota/limit";
11
11
  const GOOGLE_QUOTA_API_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
12
- const GOOGLE_TOKEN_REFRESH_URL = "https://oauth2.googleapis.com/token";
13
12
  const REQUEST_TIMEOUT_MS = 10_000;
14
13
  const DAY_SECONDS = 86_400;
15
14
  const HOUR_SECONDS = 3_600;
16
15
  const DEFAULT_ANTIGRAVITY_PROJECT_ID = "rising-fact-p41fc";
17
- const GOOGLE_CLIENT_ID = process.env.PIX_ANTIGRAVITY_GOOGLE_CLIENT_ID ?? process.env.ANTIGRAVITY_GOOGLE_CLIENT_ID ?? "";
18
- const GOOGLE_CLIENT_SECRET = process.env.PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET ?? process.env.ANTIGRAVITY_GOOGLE_CLIENT_SECRET ?? "";
19
16
  const OPENAI_QUOTA_PROVIDERS = new Set(["openai", "openai-codex"]);
20
17
  const ZHIPU_QUOTA_PROVIDERS = new Set(["zai", "zhipuai-coding-plan"]);
21
18
  const ANTIGRAVITY_QUOTA_PROVIDERS = new Set(["antigravity", "google-antigravity"]);
@@ -401,8 +398,13 @@ export function googleAntigravityUsageStatusFromResponse(data, descriptor, now =
401
398
  };
402
399
  }
403
400
  async function queryGoogleAntigravityModelUsage(descriptor) {
404
- const { access_token } = await refreshGoogleAccessToken(descriptor.account.refreshToken);
405
- const response = await fetchGoogleAntigravityQuota(access_token, descriptor.account.projectId);
401
+ const now = Date.now();
402
+ const cachedResponse = googleQuotaResponseFromCachedQuota(descriptor.account.cachedQuota, descriptor.account.cachedQuotaUpdatedAt, now);
403
+ if (cachedResponse)
404
+ return googleAntigravityUsageStatusFromResponse(cachedResponse, descriptor, now);
405
+ if (!descriptor.account.accessToken)
406
+ return undefined;
407
+ const response = await fetchGoogleAntigravityQuota(descriptor.account.accessToken, descriptor.account.projectId);
406
408
  return googleAntigravityUsageStatusFromResponse(response, descriptor);
407
409
  }
408
410
  const GOOGLE_ACCOUNT_QUOTA_WINDOWS = [
@@ -417,10 +419,12 @@ async function queryGoogleAntigravityAccountUsage(now) {
417
419
  const results = await Promise.all(accounts.map(async (account) => {
418
420
  const accountLabel = account.email ?? maskCredential(account.refreshToken);
419
421
  try {
420
- const { access_token } = await refreshGoogleAccessToken(account.refreshToken);
421
- const response = await fetchGoogleAntigravityQuota(access_token, account.projectId);
422
- const windows = GOOGLE_ACCOUNT_QUOTA_WINDOWS.map((window) => googleAccountWindowFromResponse(response, window.label, window.quotaModelKey, now))
423
- .filter((window) => window !== undefined);
422
+ const response = account.cachedQuota ? googleQuotaResponseFromCachedQuota(account.cachedQuota, account.cachedQuotaUpdatedAt, now) : undefined;
423
+ const windows = response ? googleAccountWindowsFromResponse(response, now) : [];
424
+ if (windows.length === 0 && account.accessToken) {
425
+ const liveResponse = await fetchGoogleAntigravityQuota(account.accessToken, account.projectId);
426
+ windows.push(...googleAccountWindowsFromResponse(liveResponse, now));
427
+ }
424
428
  return {
425
429
  account: accountLabel,
426
430
  windows,
@@ -449,15 +453,20 @@ function readAllAntigravityQuotaAccounts() {
449
453
  return [];
450
454
  const accounts = storedAntigravityAccounts(credential);
451
455
  if (accounts.length > 0) {
456
+ const activeIndex = clampAccountIndex(credential.activeIndex, accounts.length);
457
+ const activeAccess = antigravityAccessFromCredential(credential);
452
458
  return accounts.map((account, accountIndex) => antigravityQuotaAccount(account, {
453
459
  ...(credential.email ? { fallbackEmail: credential.email } : {}),
460
+ ...(accountIndex === activeIndex && activeAccess ? { accessToken: activeAccess.accessToken } : {}),
454
461
  accountIndex,
455
462
  accountCount: accounts.length,
456
463
  })).filter((account) => account !== undefined);
457
464
  }
458
465
  const fallbackAccount = antigravityAccountFromCredential(credential);
466
+ const fallbackAccess = antigravityAccessFromCredential(credential);
459
467
  const account = fallbackAccount ? antigravityQuotaAccount(fallbackAccount, {
460
468
  ...(credential.email ? { fallbackEmail: credential.email } : {}),
469
+ ...(fallbackAccess ? { accessToken: fallbackAccess.accessToken } : {}),
461
470
  }) : undefined;
462
471
  return account ? [account] : [];
463
472
  }
@@ -480,12 +489,15 @@ function antigravityAccountFromCredential(credential) {
480
489
  const refresh = splitAntigravityRefresh(credential.refresh);
481
490
  if (!refresh.refreshToken)
482
491
  return undefined;
492
+ const activeStoredAccount = credential.accounts?.[clampAccountIndex(credential.activeIndex, credential.accounts.length)];
483
493
  return {
484
494
  refreshToken: refresh.refreshToken,
485
495
  projectId: refresh.projectId || refresh.managedProjectId || DEFAULT_ANTIGRAVITY_PROJECT_ID,
486
496
  enabled: true,
487
497
  ...(credential.email ? { email: credential.email } : {}),
488
498
  ...(refresh.managedProjectId ? { managedProjectId: refresh.managedProjectId } : {}),
499
+ ...(activeStoredAccount?.cachedQuota ? { cachedQuota: activeStoredAccount.cachedQuota } : {}),
500
+ ...(typeof activeStoredAccount?.cachedQuotaUpdatedAt === "number" ? { cachedQuotaUpdatedAt: activeStoredAccount.cachedQuotaUpdatedAt } : {}),
489
501
  };
490
502
  }
491
503
  function antigravityQuotaAccount(account, options = {}) {
@@ -498,11 +510,65 @@ function antigravityQuotaAccount(account, options = {}) {
498
510
  refreshToken,
499
511
  projectId,
500
512
  cacheKey: email ? email.toLowerCase() : shortHash(refreshToken),
513
+ ...(options.accessToken ? { accessToken: options.accessToken } : {}),
514
+ ...(account.cachedQuota ? { cachedQuota: account.cachedQuota } : {}),
515
+ ...(typeof account.cachedQuotaUpdatedAt === "number" ? { cachedQuotaUpdatedAt: account.cachedQuotaUpdatedAt } : {}),
501
516
  ...(email ? { email } : {}),
502
517
  ...(typeof options.accountIndex === "number" ? { accountIndex: options.accountIndex } : {}),
503
518
  ...(typeof options.accountCount === "number" ? { accountCount: options.accountCount } : {}),
504
519
  };
505
520
  }
521
+ function googleQuotaResponseFromCachedQuota(cachedQuota, cachedQuotaUpdatedAt, now = Date.now()) {
522
+ if (!cachedQuota)
523
+ return undefined;
524
+ const models = {};
525
+ addCachedQuotaModels(models, cachedQuota.claude, ["claude-opus-4-6-thinking", "claude-sonnet-4-6"], cachedQuotaUpdatedAt, now);
526
+ addCachedQuotaModels(models, cachedQuota["gemini-flash"], ["gemini-2.5-flash", "gemini-3-flash"], cachedQuotaUpdatedAt, now);
527
+ addCachedQuotaModels(models, cachedQuota["gemini-pro"], ["gemini-3.1-pro-low"], cachedQuotaUpdatedAt, now);
528
+ return Object.keys(models).length > 0 ? { models } : undefined;
529
+ }
530
+ function addCachedQuotaModels(models, quota, quotaModelKeys, cachedQuotaUpdatedAt, now) {
531
+ if (!quota || !Number.isFinite(quota.remainingFraction))
532
+ return;
533
+ const remainingFraction = quota.remainingFraction;
534
+ const resetTime = cachedQuotaResetTimeForDisplay(quota.resetTime, cachedQuotaUpdatedAt, now);
535
+ for (const quotaModelKey of quotaModelKeys) {
536
+ models[quotaModelKey] = {
537
+ quotaInfo: {
538
+ remainingFraction,
539
+ ...(resetTime ? { resetTime } : {}),
540
+ },
541
+ };
542
+ }
543
+ }
544
+ function cachedQuotaResetTimeForDisplay(resetTime, cachedQuotaUpdatedAt, now) {
545
+ if (!resetTime)
546
+ return undefined;
547
+ const resetAt = Date.parse(resetTime);
548
+ if (!Number.isFinite(resetAt) || resetAt > now)
549
+ return resetTime;
550
+ const cachedAt = normalizeTimestampMillis(cachedQuotaUpdatedAt);
551
+ if (!Number.isFinite(cachedAt) || resetAt <= cachedAt)
552
+ return resetTime;
553
+ return new Date(now + (resetAt - cachedAt)).toISOString();
554
+ }
555
+ function normalizeTimestampMillis(value) {
556
+ if (!Number.isFinite(value))
557
+ return Number.NaN;
558
+ const timestamp = value;
559
+ return timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp;
560
+ }
561
+ function antigravityAccessFromCredential(credential) {
562
+ if (credential.type !== "oauth" || !credential.access || isExpired(credential))
563
+ return undefined;
564
+ const [accessToken = "", projectId = ""] = credential.access.split("|");
565
+ if (!accessToken)
566
+ return undefined;
567
+ return {
568
+ accessToken,
569
+ ...(projectId ? { projectId } : {}),
570
+ };
571
+ }
506
572
  function splitAntigravityRefresh(refresh) {
507
573
  const [refreshToken = "", projectId = "", managedProjectId = ""] = refresh.split("|");
508
574
  return {
@@ -519,26 +585,6 @@ function clampAccountIndex(index, accountCount) {
519
585
  function shortHash(value) {
520
586
  return createHash("sha256").update(value).digest("hex").slice(0, 12);
521
587
  }
522
- async function refreshGoogleAccessToken(refreshToken) {
523
- if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
524
- throw new Error("Antigravity Google OAuth credentials are not configured; set PIX_ANTIGRAVITY_GOOGLE_CLIENT_ID and PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET.");
525
- }
526
- const response = await fetchWithTimeout(GOOGLE_TOKEN_REFRESH_URL, {
527
- method: "POST",
528
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
529
- body: new URLSearchParams({
530
- client_id: GOOGLE_CLIENT_ID,
531
- client_secret: GOOGLE_CLIENT_SECRET,
532
- refresh_token: refreshToken,
533
- grant_type: "refresh_token",
534
- }),
535
- });
536
- if (!response.ok) {
537
- const errorText = await response.text();
538
- throw new Error(`Google token refresh failed (${response.status}): ${errorText}`);
539
- }
540
- return response.json();
541
- }
542
588
  async function fetchGoogleAntigravityQuota(accessToken, projectId) {
543
589
  const response = await fetchWithTimeout(GOOGLE_QUOTA_API_URL, {
544
590
  method: "POST",
@@ -633,18 +679,51 @@ function selectOpenAIRateLimitForModel(data, modelKey) {
633
679
  return false;
634
680
  return openAIModelMatchesAdditionalLimit(modelKey, limit);
635
681
  });
682
+ // Prefer exact named per-model buckets when the API exposes them, but keep the
683
+ // top-level bucket as a fallback. Some Codex responses currently expose a
684
+ // usable selected-model/account bucket only at the top level while also
685
+ // listing unrelated named additional buckets; hiding the fallback makes the
686
+ // status bar disappear completely for those models.
636
687
  return additionalLimit?.rate_limit ?? data.rate_limit;
637
688
  }
638
689
  function openAIModelMatchesAdditionalLimit(modelKey, limit) {
639
- const normalizedModel = normalizeOpenAILimitName(modelKey.split("/").at(-1) ?? modelKey);
640
- const normalizedLimitName = normalizeOpenAILimitName(limit.limit_name);
641
- const normalizedMeteredFeature = limit.metered_feature ? normalizeOpenAILimitName(limit.metered_feature) : "";
642
- return (!!normalizedLimitName && (normalizedModel.includes(normalizedLimitName) || normalizedLimitName.includes(normalizedModel)))
643
- || (!!normalizedMeteredFeature && (normalizedModel.includes(normalizedMeteredFeature) || normalizedMeteredFeature.includes(normalizedModel)));
690
+ const modelId = modelKey.split("/").at(-1) ?? modelKey;
691
+ return openAIModelIdMatchesLimitCandidate(modelId, limit.limit_name)
692
+ || (limit.metered_feature ? openAIModelIdMatchesLimitCandidate(modelId, limit.metered_feature) : false);
693
+ }
694
+ function openAIModelIdMatchesLimitCandidate(modelId, candidate) {
695
+ const modelTokens = openAILimitTokens(modelId);
696
+ const candidateTokens = openAILimitTokens(candidate);
697
+ if (modelTokens.length === 0 || candidateTokens.length === 0)
698
+ return false;
699
+ if (containsTokenSequence(candidateTokens, modelTokens))
700
+ return true;
701
+ // Support compact names such as o4mini while avoiding prefix matches such as
702
+ // gpt-5 accidentally matching gpt-5.5.
703
+ return normalizeOpenAILimitName(candidate) === normalizeOpenAILimitName(modelId);
644
704
  }
645
705
  function normalizeOpenAILimitName(value) {
646
706
  return value.toLowerCase().replace(/[^a-z0-9]+/gu, "");
647
707
  }
708
+ function openAILimitTokens(value) {
709
+ return value.toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length > 0);
710
+ }
711
+ function containsTokenSequence(tokens, sequence) {
712
+ if (sequence.length > tokens.length)
713
+ return false;
714
+ for (let start = 0; start <= tokens.length - sequence.length; start += 1) {
715
+ let matches = true;
716
+ for (let offset = 0; offset < sequence.length; offset += 1) {
717
+ if (tokens[start + offset] !== sequence[offset]) {
718
+ matches = false;
719
+ break;
720
+ }
721
+ }
722
+ if (matches)
723
+ return true;
724
+ }
725
+ return false;
726
+ }
648
727
  function selectWeeklyWindow(windows) {
649
728
  return windows
650
729
  .filter((window) => window.limit_window_seconds >= 6 * DAY_SECONDS)
@@ -683,6 +762,11 @@ function googleAccountWindowFromResponse(data, label, quotaModelKey, now) {
683
762
  windowSeconds: Math.max(0, Math.round((resetAt - now) / 1000)),
684
763
  };
685
764
  }
765
+ function googleAccountWindowsFromResponse(data, now) {
766
+ return GOOGLE_ACCOUNT_QUOTA_WINDOWS
767
+ .map((window) => googleAccountWindowFromResponse(data, window.label, window.quotaModelKey, now))
768
+ .filter((window) => window !== undefined);
769
+ }
686
770
  function clampPercent(percent) {
687
771
  return Math.max(0, Math.min(100, percent));
688
772
  }
@@ -706,6 +790,8 @@ function formatQuotaBar(percent, width) {
706
790
  return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
707
791
  }
708
792
  function formatDurationLong(resetAt, now) {
793
+ if (resetAt <= now)
794
+ return "reset";
709
795
  const totalMinutes = Math.max(0, Math.ceil((resetAt - now) / 60_000));
710
796
  const days = Math.floor(totalMinutes / 1440);
711
797
  const hours = Math.floor((totalMinutes % 1440) / 60);
@@ -717,6 +803,8 @@ function formatDurationLong(resetAt, now) {
717
803
  return `${minutes}m`;
718
804
  }
719
805
  function formatDurationShort(resetAt, now) {
806
+ if (resetAt <= now)
807
+ return "reset";
720
808
  const totalMinutes = Math.max(0, Math.ceil((resetAt - now) / 60_000));
721
809
  const days = Math.floor(totalMinutes / 1440);
722
810
  const hours = Math.floor((totalMinutes % 1440) / 60);
@@ -737,6 +825,8 @@ function formatUsageWindow(_prefix, window, now) {
737
825
  return `${window.remainingPercent}% ${formatCompactProgressBar(window.remainingPercent)} ${formatResetCountdown(window.resetAt, now)}`;
738
826
  }
739
827
  function formatResetCountdown(resetAt, now) {
828
+ if (resetAt <= now)
829
+ return "reset";
740
830
  const totalMinutes = Math.max(0, Math.ceil((resetAt - now) / 60_000));
741
831
  const days = Math.floor(totalMinutes / 1440);
742
832
  const hours = Math.floor((totalMinutes % 1440) / 60);
@@ -148,7 +148,7 @@ export class AppPopupActionController {
148
148
  this.host.render();
149
149
  try {
150
150
  if (selected.value === "copy") {
151
- this.workspaceActions.copyUserMessage(selected.entryId);
151
+ await this.workspaceActions.copyUserMessage(selected.entryId);
152
152
  return true;
153
153
  }
154
154
  if (selected.value === "fork") {
@@ -0,0 +1,17 @@
1
+ export type AsyncProcessResult = {
2
+ status: number | null;
3
+ signal: NodeJS.Signals | null;
4
+ stdout: string;
5
+ stderr: string;
6
+ error?: Error;
7
+ timedOut?: boolean;
8
+ };
9
+ export type RunProcessOptions = {
10
+ cwd?: string;
11
+ env?: NodeJS.ProcessEnv;
12
+ input?: string;
13
+ timeoutMs?: number;
14
+ maxBufferBytes?: number;
15
+ };
16
+ export declare function runProcess(command: string, args?: readonly string[], options?: RunProcessOptions): Promise<AsyncProcessResult>;
17
+ export declare function commandExists(command: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
@@ -0,0 +1,68 @@
1
+ import { spawn } from "node:child_process";
2
+ const DEFAULT_MAX_BUFFER_BYTES = 1024 * 1024;
3
+ export async function runProcess(command, args = [], options = {}) {
4
+ const maxBufferBytes = Math.max(1, options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES);
5
+ return new Promise((resolve) => {
6
+ let stdout = "";
7
+ let stderr = "";
8
+ let error;
9
+ let timedOut = false;
10
+ const child = spawn(command, [...args], {
11
+ cwd: options.cwd,
12
+ env: options.env,
13
+ stdio: ["pipe", "pipe", "pipe"],
14
+ });
15
+ const append = (current, chunk) => {
16
+ const next = `${current}${chunk.toString("utf8")}`;
17
+ return next.length > maxBufferBytes ? next.slice(-maxBufferBytes) : next;
18
+ };
19
+ const timer = options.timeoutMs === undefined
20
+ ? undefined
21
+ : setTimeout(() => {
22
+ timedOut = true;
23
+ child.kill("SIGTERM");
24
+ }, options.timeoutMs);
25
+ timer?.unref?.();
26
+ child.stdout.on("data", (chunk) => {
27
+ stdout = append(stdout, chunk);
28
+ });
29
+ child.stderr.on("data", (chunk) => {
30
+ stderr = append(stderr, chunk);
31
+ });
32
+ child.once("error", (err) => {
33
+ error = err;
34
+ });
35
+ child.once("close", (status, signal) => {
36
+ if (timer)
37
+ clearTimeout(timer);
38
+ resolve({
39
+ status,
40
+ signal,
41
+ stdout,
42
+ stderr,
43
+ ...(error === undefined ? {} : { error }),
44
+ ...(timedOut ? { timedOut } : {}),
45
+ });
46
+ });
47
+ if (options.input === undefined)
48
+ child.stdin.end();
49
+ else
50
+ child.stdin.end(options.input);
51
+ });
52
+ }
53
+ export async function commandExists(command, env = process.env) {
54
+ if (process.platform === "win32") {
55
+ const names = [command, command.replace(/\.exe$/iu, ".cmd"), command.replace(/\.exe$/iu, ".bat")];
56
+ for (const name of names) {
57
+ const result = await runProcess("where", [name], { env, maxBufferBytes: 256 });
58
+ if (result.status === 0)
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ const result = await runProcess("sh", ["-lc", `command -v ${shellQuote(command)}`], { env, maxBufferBytes: 256 });
64
+ return result.status === 0;
65
+ }
66
+ function shellQuote(value) {
67
+ return `'${value.replaceAll("'", `'\\''`)}'`;
68
+ }
@@ -1,6 +1,7 @@
1
1
  import { applyOutputFilters } from "../../config.js";
2
2
  import { renderMarkdownTextLines } from "../../markdown-format.js";
3
3
  import { attachImageClickTargets } from "../screen/image-click-targets.js";
4
+ import { APP_ICONS } from "../icons.js";
4
5
  import { horizontalPaddingLayout, padHorizontalText, wrapText } from "./render-text.js";
5
6
  import { renderConversationShellEntry } from "./conversation-shell-renderer.js";
6
7
  import { renderConversationToolEntry, renderThinkingEntry } from "./conversation-tool-renderer.js";
@@ -14,10 +15,11 @@ export function renderConversationEntry(entry, width, options) {
14
15
  ...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
15
16
  ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
16
17
  });
17
- const queuedLine = (text, entryId) => ({
18
+ const queuedLine = (text, entryId, segments) => ({
18
19
  text: padHorizontalText(text, width),
19
20
  variant: "muted",
20
21
  backgroundOverride: options.colors.userMessageBackground,
22
+ ...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
21
23
  target: { kind: "queue-message", id: entryId },
22
24
  });
23
25
  const userMessageLines = (userEntry) => {
@@ -29,11 +31,11 @@ export function renderConversationEntry(entry, width, options) {
29
31
  lines.push(userLine("", userEntry.id));
30
32
  return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
31
33
  };
32
- const queuedMessageLines = (queuedEntry) => [
33
- queuedLine("", queuedEntry.id),
34
- ...wrapText(`↳ queued ${queuedEntry.mode}: ${queuedEntry.text}`, userContentWidth).map((text) => queuedLine(text, queuedEntry.id)),
35
- queuedLine("", queuedEntry.id),
36
- ];
34
+ const queuedMessageLines = (queuedEntry) => {
35
+ const icon = queuedEntry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
36
+ const contentLines = wrapText(`${icon} ${queuedEntry.text}`, userContentWidth);
37
+ return contentLines.map((text, index) => queuedLine(text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined));
38
+ };
37
39
  switch (entry.kind) {
38
40
  case "system":
39
41
  return wrapText(`system: ${entry.text}`, width).map((text) => ({ text, variant: "muted" }));
@@ -49,12 +49,13 @@ export function renderThinkingEntry(entry, width, options) {
49
49
  const rule = resolveThinkingToolRule(options.pixConfig);
50
50
  const markdownText = entry.text ? formatMarkdownTables(entry.text, Math.max(1, width - 2)) : "";
51
51
  const expandedText = trimTrailingBlankLines(markdownText);
52
- const compactExpandedText = options.superCompactTools ? removeBlankLines(expandedText) : expandedText;
53
52
  const forceExpanded = Boolean(options.allThinkingExpanded);
53
+ const compactExpandedText = options.superCompactTools && forceExpanded ? removeBlankLines(expandedText) : expandedText;
54
+ const expanded = forceExpanded || (entry.expanded && expandedText.trim().length > 0);
54
55
  return renderToolBlock({
55
56
  id: entry.id,
56
57
  toolName: THINKING_TOOL_NAME,
57
- expanded: entry.expanded || forceExpanded,
58
+ expanded,
58
59
  status: entry.status,
59
60
  isError: false,
60
61
  output: markdownText,
@@ -10,6 +10,7 @@ export type EditorLayoutRendererHost = {
10
10
  readonly subagentsPanelExpanded: boolean;
11
11
  readonly subagentsWidgetState: SubagentsWidgetState | undefined;
12
12
  readonly voicePartialText: string | undefined;
13
+ readonly autocompleteSuggestion: string | undefined;
13
14
  renderExtensionInputComponent(width: number): string[] | undefined;
14
15
  extensionInputUsesEditor(): boolean;
15
16
  widgetTuiHandle(): WidgetTuiHandle;
@@ -145,7 +145,7 @@ export class EditorLayoutRenderer {
145
145
  ? this.limitExtensionInputLines(extensionLines, Math.max(0, maxRows - (usesEditor ? 1 : 0)))
146
146
  : [];
147
147
  const editorMaxRows = usesEditor ? Math.max(1, maxRows - customLines.length) : 1;
148
- const rendered = this.host.inputEditor.render(contentWidth, editorMaxRows, "", "");
148
+ const rendered = this.host.inputEditor.render(contentWidth, editorMaxRows, "", "", usesEditor ? this.host.autocompleteSuggestion ?? "" : "");
149
149
  const visibleLines = rendered.visualLines.slice(rendered.scrollOffset, rendered.scrollOffset + editorMaxRows);
150
150
  const scrollBar = usesEditor
151
151
  ? inputScrollBarMetrics(rendered.visualLines.length, visibleLines.length, rendered.scrollOffset)
@@ -157,6 +157,12 @@ export class EditorLayoutRenderer {
157
157
  end: span.end + left,
158
158
  })))
159
159
  : [];
160
+ const editorSuggestionSpans = usesEditor
161
+ ? visibleLines.map((vl) => (vl.suggestionSpans ?? []).map((span) => ({
162
+ start: span.start + left,
163
+ end: span.end + left,
164
+ })))
165
+ : [];
160
166
  const paddedCustomLines = customLines.map((line) => frameInputLine(padHorizontalText(line, width)));
161
167
  return {
162
168
  lines: [...paddedCustomLines, ...editorLines],
@@ -172,6 +178,10 @@ export class EditorLayoutRenderer {
172
178
  ...paddedCustomLines.map(() => []),
173
179
  ...editorTagSpans,
174
180
  ],
181
+ suggestionSpans: [
182
+ ...paddedCustomLines.map(() => []),
183
+ ...editorSuggestionSpans,
184
+ ],
175
185
  };
176
186
  }
177
187
  limitExtensionInputLines(lines, maxRows) {