pi-ui-extend 0.1.8 → 0.1.11

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 (191) hide show
  1. package/README.md +57 -2
  2. package/bin/pix.mjs +4 -4
  3. package/dist/app/app.d.ts +4 -0
  4. package/dist/app/app.js +112 -45
  5. package/dist/app/{cli.d.ts → cli/cli.d.ts} +1 -1
  6. package/dist/app/{cli.js → cli/cli.js} +1 -1
  7. package/dist/app/{install.d.ts → cli/install.d.ts} +2 -0
  8. package/dist/app/{install.js → cli/install.js} +18 -3
  9. package/dist/app/{command-controller.d.ts → commands/command-controller.d.ts} +1 -1
  10. package/dist/app/{command-controller.js → commands/command-controller.js} +4 -0
  11. package/dist/app/{command-host.d.ts → commands/command-host.d.ts} +7 -3
  12. package/dist/app/{command-model-actions.d.ts → commands/command-model-actions.d.ts} +6 -1
  13. package/dist/app/{command-model-actions.js → commands/command-model-actions.js} +106 -2
  14. package/dist/app/{command-navigation-actions.d.ts → commands/command-navigation-actions.d.ts} +7 -2
  15. package/dist/app/{command-navigation-actions.js → commands/command-navigation-actions.js} +42 -19
  16. package/dist/app/{command-registry.d.ts → commands/command-registry.d.ts} +5 -1
  17. package/dist/app/{command-registry.js → commands/command-registry.js} +32 -0
  18. package/dist/app/{command-runtime.js → commands/command-runtime.js} +1 -1
  19. package/dist/app/{command-session-actions.d.ts → commands/command-session-actions.d.ts} +1 -0
  20. package/dist/app/{command-session-actions.js → commands/command-session-actions.js} +18 -8
  21. package/dist/app/{shell-controller.d.ts → commands/shell-controller.d.ts} +2 -1
  22. package/dist/app/{shell-controller.js → commands/shell-controller.js} +2 -2
  23. package/dist/app/{slash-commands.d.ts → commands/slash-commands.d.ts} +2 -2
  24. package/dist/app/{slash-commands.js → commands/slash-commands.js} +1 -1
  25. package/dist/app/constants.d.ts +1 -1
  26. package/dist/app/constants.js +1 -1
  27. package/dist/app/{extension-actions-controller.d.ts → extensions/extension-actions-controller.d.ts} +1 -1
  28. package/dist/app/{extension-actions-controller.js → extensions/extension-actions-controller.js} +1 -1
  29. package/dist/app/{extension-ui-controller.d.ts → extensions/extension-ui-controller.d.ts} +3 -3
  30. package/dist/app/{extension-ui-controller.js → extensions/extension-ui-controller.js} +3 -3
  31. package/dist/app/icons.js +1 -1
  32. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  33. package/dist/app/input/autocomplete-controller.js +352 -0
  34. package/dist/app/{input-action-controller.d.ts → input/input-action-controller.d.ts} +8 -7
  35. package/dist/app/{input-action-controller.js → input/input-action-controller.js} +24 -3
  36. package/dist/app/{input-controller.d.ts → input/input-controller.d.ts} +4 -3
  37. package/dist/app/{input-controller.js → input/input-controller.js} +3 -1
  38. package/dist/app/{input-paste-handler.d.ts → input/input-paste-handler.d.ts} +2 -1
  39. package/dist/app/{input-paste-handler.js → input/input-paste-handler.js} +25 -21
  40. package/dist/app/{native-modifiers.js → input/native-modifiers.js} +2 -2
  41. package/dist/app/{prompt-enhancer-controller.d.ts → input/prompt-enhancer-controller.d.ts} +5 -5
  42. package/dist/app/{prompt-enhancer-controller.js → input/prompt-enhancer-controller.js} +3 -3
  43. package/dist/app/{voice-controller.d.ts → input/voice-controller.d.ts} +3 -1
  44. package/dist/app/{voice-controller.js → input/voice-controller.js} +29 -17
  45. package/dist/app/{model-ref.d.ts → model/model-ref.d.ts} +1 -1
  46. package/dist/app/{model-ref.js → model/model-ref.js} +1 -1
  47. package/dist/app/{model-usage-controller.js → model/model-usage-controller.js} +1 -1
  48. package/dist/app/{model-usage-status.d.ts → model/model-usage-status.d.ts} +10 -1
  49. package/dist/app/{model-usage-status.js → model/model-usage-status.js} +125 -35
  50. package/dist/app/{menu-items-controller.d.ts → popup/menu-items-controller.d.ts} +4 -4
  51. package/dist/app/{menu-items-controller.js → popup/menu-items-controller.js} +5 -5
  52. package/dist/app/{popup-action-controller.d.ts → popup/popup-action-controller.d.ts} +4 -4
  53. package/dist/app/{popup-action-controller.js → popup/popup-action-controller.js} +3 -3
  54. package/dist/app/{popup-menu-controller.d.ts → popup/popup-menu-controller.d.ts} +4 -4
  55. package/dist/app/{popup-menu-controller.js → popup/popup-menu-controller.js} +7 -7
  56. package/dist/app/process.d.ts +17 -0
  57. package/dist/app/process.js +68 -0
  58. package/dist/app/{conversation-entry-renderer.d.ts → rendering/conversation-entry-renderer.d.ts} +3 -3
  59. package/dist/app/{conversation-entry-renderer.js → rendering/conversation-entry-renderer.js} +20 -9
  60. package/dist/app/{conversation-shell-renderer.d.ts → rendering/conversation-shell-renderer.d.ts} +1 -1
  61. package/dist/app/{conversation-shell-renderer.js → rendering/conversation-shell-renderer.js} +1 -1
  62. package/dist/app/{conversation-tool-renderer.d.ts → rendering/conversation-tool-renderer.d.ts} +3 -3
  63. package/dist/app/{conversation-tool-renderer.js → rendering/conversation-tool-renderer.js} +10 -9
  64. package/dist/app/{conversation-viewport.d.ts → rendering/conversation-viewport.d.ts} +3 -3
  65. package/dist/app/{dcp-stats.js → rendering/dcp-stats.js} +1 -1
  66. package/dist/app/{editor-layout-renderer.d.ts → rendering/editor-layout-renderer.d.ts} +4 -3
  67. package/dist/app/{editor-layout-renderer.js → rendering/editor-layout-renderer.js} +13 -3
  68. package/dist/app/{editor-panels.d.ts → rendering/editor-panels.d.ts} +2 -2
  69. package/dist/app/{editor-panels.js → rendering/editor-panels.js} +4 -4
  70. package/dist/app/{message-content.d.ts → rendering/message-content.d.ts} +1 -1
  71. package/dist/app/{message-content.js → rendering/message-content.js} +66 -8
  72. package/dist/app/{render-controller.d.ts → rendering/render-controller.d.ts} +6 -6
  73. package/dist/app/{render-controller.js → rendering/render-controller.js} +11 -6
  74. package/dist/app/{render-text.d.ts → rendering/render-text.d.ts} +5 -2
  75. package/dist/app/{render-text.js → rendering/render-text.js} +53 -5
  76. package/dist/app/{status-line-renderer.d.ts → rendering/status-line-renderer.d.ts} +8 -4
  77. package/dist/app/{status-line-renderer.js → rendering/status-line-renderer.js} +73 -29
  78. package/dist/app/{tab-line-renderer.d.ts → rendering/tab-line-renderer.d.ts} +3 -3
  79. package/dist/app/{tab-line-renderer.js → rendering/tab-line-renderer.js} +2 -2
  80. package/dist/app/{toast-controller.d.ts → rendering/toast-controller.d.ts} +1 -1
  81. package/dist/app/{toast-controller.js → rendering/toast-controller.js} +2 -2
  82. package/dist/app/{toast-renderer.d.ts → rendering/toast-renderer.d.ts} +3 -3
  83. package/dist/app/{toast-renderer.js → rendering/toast-renderer.js} +3 -3
  84. package/dist/app/{tool-block-renderer.d.ts → rendering/tool-block-renderer.d.ts} +5 -5
  85. package/dist/app/{tool-block-renderer.js → rendering/tool-block-renderer.js} +15 -33
  86. package/dist/app/runtime.d.ts +6 -1
  87. package/dist/app/runtime.js +35 -2
  88. package/dist/app/{blink-controller.js → screen/blink-controller.js} +1 -1
  89. package/dist/app/{clipboard.d.ts → screen/clipboard.d.ts} +2 -2
  90. package/dist/app/{clipboard.js → screen/clipboard.js} +13 -18
  91. package/dist/app/{image-click-targets.d.ts → screen/image-click-targets.d.ts} +2 -2
  92. package/dist/app/{image-opener.d.ts → screen/image-opener.d.ts} +1 -1
  93. package/dist/app/{mouse-controller.d.ts → screen/mouse-controller.d.ts} +17 -10
  94. package/dist/app/{mouse-controller.js → screen/mouse-controller.js} +72 -29
  95. package/dist/app/{screen-selection.d.ts → screen/screen-selection.d.ts} +1 -1
  96. package/dist/app/{screen-styler.d.ts → screen/screen-styler.d.ts} +6 -3
  97. package/dist/app/{screen-styler.js → screen/screen-styler.js} +7 -6
  98. package/dist/app/{scroll-controller.d.ts → screen/scroll-controller.d.ts} +3 -3
  99. package/dist/app/{scroll-controller.js → screen/scroll-controller.js} +1 -1
  100. package/dist/app/{status-controller.d.ts → screen/status-controller.d.ts} +5 -2
  101. package/dist/app/{status-controller.js → screen/status-controller.js} +24 -9
  102. package/dist/app/{queued-message-controller.d.ts → session/queued-message-controller.d.ts} +9 -3
  103. package/dist/app/{queued-message-controller.js → session/queued-message-controller.js} +34 -23
  104. package/dist/app/{request-history.js → session/request-history.js} +2 -2
  105. package/dist/app/session/resume-session-loader.d.ts +15 -0
  106. package/dist/app/session/resume-session-loader.js +204 -0
  107. package/dist/app/{session-event-controller.d.ts → session/session-event-controller.d.ts} +8 -4
  108. package/dist/app/{session-event-controller.js → session/session-event-controller.js} +75 -8
  109. package/dist/app/{session-history.d.ts → session/session-history.d.ts} +1 -1
  110. package/dist/app/{session-history.js → session/session-history.js} +7 -6
  111. package/dist/app/{session-lifecycle-controller.d.ts → session/session-lifecycle-controller.d.ts} +7 -2
  112. package/dist/app/{session-lifecycle-controller.js → session/session-lifecycle-controller.js} +13 -5
  113. package/dist/app/{session-search.d.ts → session/session-search.d.ts} +1 -1
  114. package/dist/app/{session-search.js → session/session-search.js} +3 -3
  115. package/dist/app/{tabs-controller.d.ts → session/tabs-controller.d.ts} +11 -2
  116. package/dist/app/{tabs-controller.js → session/tabs-controller.js} +105 -9
  117. package/dist/app/{subagents-files.d.ts → subagents/subagents-files.d.ts} +1 -1
  118. package/dist/app/{subagents-files.js → subagents/subagents-files.js} +1 -1
  119. package/dist/app/{subagents-model.d.ts → subagents/subagents-model.d.ts} +1 -1
  120. package/dist/app/{subagents-model.js → subagents/subagents-model.js} +4 -4
  121. package/dist/app/{subagents-widget-controller.d.ts → subagents/subagents-widget-controller.d.ts} +1 -1
  122. package/dist/app/{subagents-widget-controller.js → subagents/subagents-widget-controller.js} +2 -2
  123. package/dist/app/{nerd-font-controller.js → terminal/nerd-font-controller.js} +16 -17
  124. package/dist/app/{terminal-bell-sound-controller.js → terminal/terminal-bell-sound-controller.js} +1 -1
  125. package/dist/app/{terminal-controller.d.ts → terminal/terminal-controller.d.ts} +1 -0
  126. package/dist/app/{terminal-controller.js → terminal/terminal-controller.js} +3 -2
  127. package/dist/app/{todo-model.d.ts → todo/todo-model.d.ts} +1 -1
  128. package/dist/app/{todo-model.js → todo/todo-model.js} +3 -3
  129. package/dist/app/{todo-widget-controller.d.ts → todo/todo-widget-controller.d.ts} +1 -1
  130. package/dist/app/{todo-widget-controller.js → todo/todo-widget-controller.js} +2 -2
  131. package/dist/app/types.d.ts +16 -2
  132. package/dist/app/{workspace-actions-controller.d.ts → workspace/workspace-actions-controller.d.ts} +2 -2
  133. package/dist/app/{workspace-actions-controller.js → workspace/workspace-actions-controller.js} +6 -6
  134. package/dist/app/{workspace-undo.d.ts → workspace/workspace-undo.d.ts} +1 -1
  135. package/dist/app/{workspace-undo.js → workspace/workspace-undo.js} +22 -20
  136. package/dist/config.d.ts +27 -0
  137. package/dist/config.js +174 -1
  138. package/dist/default-pix-config.js +38 -353
  139. package/dist/input-editor.d.ts +7 -1
  140. package/dist/input-editor.js +47 -6
  141. package/dist/main.js +2 -2
  142. package/dist/markdown-format.d.ts +1 -0
  143. package/dist/markdown-format.js +26 -1
  144. package/external/pi-tools-suite/README.md +78 -0
  145. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +4 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -1
  147. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  148. package/external/pi-tools-suite/src/dcp/prompts.ts +5 -0
  149. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +314 -193
  150. package/external/pi-tools-suite/src/index.ts +1 -0
  151. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  152. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  153. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  154. package/external/pi-tools-suite/src/opencode-import/commands.ts +86 -0
  155. package/external/pi-tools-suite/src/opencode-import/importer.ts +208 -0
  156. package/external/pi-tools-suite/src/opencode-import/index.ts +25 -0
  157. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  158. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  159. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  160. package/package.json +1 -1
  161. /package/dist/app/{startup-checks.d.ts → cli/startup-checks.d.ts} +0 -0
  162. /package/dist/app/{startup-checks.js → cli/startup-checks.js} +0 -0
  163. /package/dist/app/{startup-info.d.ts → cli/startup-info.d.ts} +0 -0
  164. /package/dist/app/{startup-info.js → cli/startup-info.js} +0 -0
  165. /package/dist/app/{update.d.ts → cli/update.d.ts} +0 -0
  166. /package/dist/app/{update.js → cli/update.js} +0 -0
  167. /package/dist/app/{command-host.js → commands/command-host.js} +0 -0
  168. /package/dist/app/{command-runtime.d.ts → commands/command-runtime.d.ts} +0 -0
  169. /package/dist/app/{shell-command.d.ts → commands/shell-command.d.ts} +0 -0
  170. /package/dist/app/{shell-command.js → commands/shell-command.js} +0 -0
  171. /package/dist/app/{extension-event-bus.d.ts → extensions/extension-event-bus.d.ts} +0 -0
  172. /package/dist/app/{extension-event-bus.js → extensions/extension-event-bus.js} +0 -0
  173. /package/dist/app/{native-modifiers.d.ts → input/native-modifiers.d.ts} +0 -0
  174. /package/dist/app/{terminal-edit-shortcuts.d.ts → input/terminal-edit-shortcuts.d.ts} +0 -0
  175. /package/dist/app/{terminal-edit-shortcuts.js → input/terminal-edit-shortcuts.js} +0 -0
  176. /package/dist/app/{model-usage-controller.d.ts → model/model-usage-controller.d.ts} +0 -0
  177. /package/dist/app/{conversation-viewport.js → rendering/conversation-viewport.js} +0 -0
  178. /package/dist/app/{dcp-stats.d.ts → rendering/dcp-stats.d.ts} +0 -0
  179. /package/dist/app/{blink-controller.d.ts → screen/blink-controller.d.ts} +0 -0
  180. /package/dist/app/{file-link-opener.d.ts → screen/file-link-opener.d.ts} +0 -0
  181. /package/dist/app/{file-link-opener.js → screen/file-link-opener.js} +0 -0
  182. /package/dist/app/{file-links.d.ts → screen/file-links.d.ts} +0 -0
  183. /package/dist/app/{file-links.js → screen/file-links.js} +0 -0
  184. /package/dist/app/{image-click-targets.js → screen/image-click-targets.js} +0 -0
  185. /package/dist/app/{image-opener.js → screen/image-opener.js} +0 -0
  186. /package/dist/app/{screen-selection.js → screen/screen-selection.js} +0 -0
  187. /package/dist/app/{request-history.d.ts → session/request-history.d.ts} +0 -0
  188. /package/dist/app/{nerd-font-controller.d.ts → terminal/nerd-font-controller.d.ts} +0 -0
  189. /package/dist/app/{terminal-bell-sound-controller.d.ts → terminal/terminal-bell-sound-controller.d.ts} +0 -0
  190. /package/dist/app/{terminal-output-buffer.d.ts → terminal/terminal-output-buffer.d.ts} +0 -0
  191. /package/dist/app/{terminal-output-buffer.js → terminal/terminal-output-buffer.js} +0 -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";
@@ -6,14 +6,16 @@ import https from "node:https";
6
6
  import { createRequire } from "node:module";
7
7
  import { join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { savePixDictationLanguage } from "../config.js";
10
- import { APP_ICONS } from "./icons.js";
9
+ import { savePixDictationLanguage } from "../../config.js";
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;
18
20
  export class AppVoiceController {
19
21
  host;
@@ -29,6 +31,7 @@ export class AppVoiceController {
29
31
  progressTimer;
30
32
  lastSystemProgressMessage;
31
33
  partialTranscript;
34
+ partialTranscriptTimer;
32
35
  startGeneration = 0;
33
36
  constructor(host, dictationConfig) {
34
37
  this.host = host;
@@ -128,7 +131,7 @@ export class AppVoiceController {
128
131
  this.state = "loading";
129
132
  this.host.render();
130
133
  const model = this.cachedModel(language, modelPath, vosk);
131
- const recorder = selectRecorderCommand();
134
+ const recorder = await selectRecorderCommand();
132
135
  const recognizer = new vosk.Recognizer({ model, sampleRate: SAMPLE_RATE });
133
136
  const audioProcess = spawn(recorder.command, recorder.args, { stdio: ["ignore", "pipe", "pipe"] });
134
137
  this.recognizer = recognizer;
@@ -283,14 +286,27 @@ export class AppVoiceController {
283
286
  if (text === this.partialTranscript)
284
287
  return;
285
288
  this.partialTranscript = text;
286
- this.host.setPartialTranscript(text);
289
+ this.schedulePartialTranscriptEmit();
287
290
  }
288
291
  clearPartialTranscript() {
289
292
  if (!this.partialTranscript)
290
293
  return;
291
294
  this.partialTranscript = undefined;
295
+ if (this.partialTranscriptTimer) {
296
+ clearTimeout(this.partialTranscriptTimer);
297
+ this.partialTranscriptTimer = undefined;
298
+ }
292
299
  this.host.setPartialTranscript(undefined);
293
300
  }
301
+ schedulePartialTranscriptEmit() {
302
+ if (this.partialTranscriptTimer)
303
+ return;
304
+ this.partialTranscriptTimer = setTimeout(() => {
305
+ this.partialTranscriptTimer = undefined;
306
+ this.host.setPartialTranscript(this.partialTranscript);
307
+ }, VOICE_PARTIAL_TRANSCRIPT_THROTTLE_MS);
308
+ this.partialTranscriptTimer.unref?.();
309
+ }
294
310
  isCurrentStart(generation) {
295
311
  return this.startGeneration === generation;
296
312
  }
@@ -367,11 +383,11 @@ async function downloadFile(url, destination, redirects = 3) {
367
383
  });
368
384
  }
369
385
  async function extractZip(zipPath, destination) {
370
- if (commandExists("unzip")) {
386
+ if (await commandExists("unzip")) {
371
387
  await runCommand("unzip", ["-q", zipPath, "-d", destination]);
372
388
  return;
373
389
  }
374
- if (process.platform === "darwin" && commandExists("ditto")) {
390
+ if (process.platform === "darwin" && await commandExists("ditto")) {
375
391
  await runCommand("ditto", ["-x", "-k", zipPath, destination]);
376
392
  return;
377
393
  }
@@ -535,7 +551,7 @@ function isVoskModule(value) {
535
551
  const record = value;
536
552
  return typeof record.Model === "function" && typeof record.Recognizer === "function";
537
553
  }
538
- function selectRecorderCommand() {
554
+ async function selectRecorderCommand() {
539
555
  const commands = [
540
556
  {
541
557
  command: "rec",
@@ -566,15 +582,11 @@ function selectRecorderCommand() {
566
582
  description: "arecord",
567
583
  });
568
584
  }
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;
585
+ for (const candidate of commands) {
586
+ if (await commandExists(candidate.command))
587
+ return candidate;
588
+ }
589
+ throw new Error("audio recorder not found: install SoX (`rec`/`sox`), ffmpeg, or arecord");
578
590
  }
579
591
  function transcriptText(result) {
580
592
  const parsed = typeof result === "string" ? parseResultString(result) : result;
@@ -1,4 +1,4 @@
1
- import type { ThinkingLevel } from "./types.js";
1
+ import type { ThinkingLevel } from "../types.js";
2
2
  export declare function parseModelRef(value: string): {
3
3
  provider: string;
4
4
  modelId: string;
@@ -1,4 +1,4 @@
1
- import { THINKING_LEVELS } from "./constants.js";
1
+ import { THINKING_LEVELS } from "../constants.js";
2
2
  export function parseModelRef(value) {
3
3
  const [modelPart, thinkingPart] = value.split(":", 2);
4
4
  if (!modelPart)
@@ -1,4 +1,4 @@
1
- import { MODEL_USAGE_POLL_INTERVAL_MS, MODEL_USAGE_STATUS_TICK_MS } from "./constants.js";
1
+ import { MODEL_USAGE_POLL_INTERVAL_MS, MODEL_USAGE_STATUS_TICK_MS } from "../constants.js";
2
2
  import { formatModelUsageStatusLabel, modelUsageDescriptor, queryModelUsageStatus, } from "./model-usage-status.js";
3
3
  export class AppModelUsageController {
4
4
  host;
@@ -1,4 +1,4 @@
1
- import type { SessionModel } from "./types.js";
1
+ import type { SessionModel } from "../types.js";
2
2
  type BaseModelUsageDescriptor = {
3
3
  readonly modelKey: string;
4
4
  };
@@ -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;
@@ -4,18 +4,15 @@ import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
- import { formatCompactProgressBar } from "../context-progress-bar.js";
7
+ import { formatCompactProgressBar } from "../../context-progress-bar.js";
8
8
  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);
@@ -1,6 +1,6 @@
1
1
  import type { AgentSessionRuntime, SessionInfo } from "@earendil-works/pi-coding-agent";
2
- import type { PopupMenuItem } from "../ui.js";
3
- import type { Entry, ModelMenuValue, QueueMessageMenuValue, ResumeMenuValue, ScopedSessionModel, SessionModel, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "./types.js";
2
+ import type { PopupMenuItem } from "../../ui.js";
3
+ import type { Entry, ModelMenuValue, QueueMessageMenuValue, ResumeMenuValue, ScopedSessionModel, SessionModel, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "../types.js";
4
4
  export type AppMenuItemsControllerHost = {
5
5
  runtime(): AgentSessionRuntime | undefined;
6
6
  getBuiltinSlashCommands(): readonly SlashCommand[];
@@ -11,9 +11,9 @@ export declare class AppMenuItemsController {
11
11
  private readonly host;
12
12
  private resumeMenuLoaderCache;
13
13
  constructor(host: AppMenuItemsControllerHost);
14
- parseSlashInput(text: string): import("./types.js").ParsedSlashInput | undefined;
14
+ parseSlashInput(text: string): import("../types.js").ParsedSlashInput | undefined;
15
15
  getResourceSlashCommands(): SlashCommand[];
16
- getSlashCommandMatches(query: string, limit?: number): import("../fuzzy.js").FuzzyMatch<SlashCommand>[];
16
+ getSlashCommandMatches(query: string, limit?: number): import("../../fuzzy.js").FuzzyMatch<SlashCommand>[];
17
17
  getSlashCommandMenuItems(query: string): PopupMenuItem<SlashCommand>[];
18
18
  modelRef(model: SessionModel): string;
19
19
  getFavoriteScopedModels(): ScopedSessionModel[];
@@ -1,10 +1,10 @@
1
1
  import { resolve } from "node:path";
2
- import { fuzzySearch } from "../fuzzy.js";
3
- import { PI_FAVORITE_MODEL_REFS, THINKING_LEVELS } from "./constants.js";
4
- import { APP_ICONS } from "./icons.js";
5
- import { parseScopedModelRef } from "./model-ref.js";
2
+ import { fuzzySearch } from "../../fuzzy.js";
3
+ import { PI_FAVORITE_MODEL_REFS, THINKING_LEVELS } from "../constants.js";
4
+ import { APP_ICONS } from "../icons.js";
5
+ import { parseScopedModelRef } from "../model/model-ref.js";
6
6
  import { buildUserMessageJumpItems, createSessionInfoMenuItemsLoader } from "./popup-menu-controller.js";
7
- import { getResourceSlashCommands, getSlashCommandMatches, parseSlashInput } from "./slash-commands.js";
7
+ import { getResourceSlashCommands, getSlashCommandMatches, parseSlashInput } from "../commands/slash-commands.js";
8
8
  export class AppMenuItemsController {
9
9
  host;
10
10
  resumeMenuLoaderCache;
@@ -1,10 +1,10 @@
1
1
  import type { AgentSession, AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
- import type { AppCommandController } from "./command-controller.js";
2
+ import type { AppCommandController } from "../commands/command-controller.js";
3
3
  import type { AppMenuItemsController } from "./menu-items-controller.js";
4
4
  import type { AppPopupMenuController } from "./popup-menu-controller.js";
5
- import type { AppQueuedMessageController } from "./queued-message-controller.js";
6
- import type { Entry, SlashCommand } from "./types.js";
7
- import type { AppWorkspaceActionsController } from "./workspace-actions-controller.js";
5
+ import type { AppQueuedMessageController } from "../session/queued-message-controller.js";
6
+ import type { Entry, SlashCommand } from "../types.js";
7
+ import type { AppWorkspaceActionsController } from "../workspace/workspace-actions-controller.js";
8
8
  export type AppPopupActionControllerHost = {
9
9
  runtime(): AgentSessionRuntime | undefined;
10
10
  getBuiltinSlashCommands(): readonly SlashCommand[];
@@ -1,5 +1,5 @@
1
- import { createId } from "./id.js";
2
- import { stringifyUnknown } from "./message-content.js";
1
+ import { createId } from "../id.js";
2
+ import { stringifyUnknown } from "../rendering/message-content.js";
3
3
  export class AppPopupActionController {
4
4
  host;
5
5
  popupMenus;
@@ -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") {
@@ -1,7 +1,7 @@
1
- import { type Theme } from "../theme.js";
2
- import { PopupMenu, type PopupMenuItem } from "../ui.js";
3
- import type { ScreenStyler } from "./screen-styler.js";
4
- import type { ActivePopupMenu, Entry, ModelMenuValue, ParsedSlashInput, PixMenuController, PixMenuItem, PixMenuOptions, PixMenuSelectOptions, PopupMenuPlacement, QueueMessageMenuValue, RenderedLine, ResumeMenuValue, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "./types.js";
1
+ import { type Theme } from "../../theme.js";
2
+ import { PopupMenu, type PopupMenuItem } from "../../ui.js";
3
+ import type { ScreenStyler } from "../screen/screen-styler.js";
4
+ import type { ActivePopupMenu, Entry, ModelMenuValue, ParsedSlashInput, PixMenuController, PixMenuItem, PixMenuOptions, PixMenuSelectOptions, PopupMenuPlacement, QueueMessageMenuValue, RenderedLine, ResumeMenuValue, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "../types.js";
5
5
  import type { AgentSession, SessionInfo } from "@earendil-works/pi-coding-agent";
6
6
  type SlashCommandMenuValue = SlashCommand;
7
7
  type ModelPopupMenuValue = ModelMenuValue;
@@ -1,11 +1,11 @@
1
1
  import { resolve } from "node:path";
2
- import { fuzzySearch } from "../fuzzy.js";
3
- import { colorLine } from "../theme.js";
4
- import { PopupMenu } from "../ui.js";
5
- import { padOrTrimPlain, ellipsizeDisplay, sanitizeText } from "./render-text.js";
6
- import { stringDisplayWidth } from "../terminal-width.js";
7
- import { RESUME_MENU_INITIAL_SESSION_ROWS, RESUME_MENU_LOAD_BATCH_ROWS, RESUME_MENU_LOAD_THRESHOLD_ROWS, RESUME_MENU_MAX_ROWS, SLASH_COMMAND_DESCRIPTION_COLUMN, SLASH_COMMAND_MENU_MAX_ROWS, THINKING_MENU_MAX_ROWS, } from "./constants.js";
8
- import { APP_ICONS } from "./icons.js";
2
+ import { fuzzySearch } from "../../fuzzy.js";
3
+ import { colorLine } from "../../theme.js";
4
+ import { PopupMenu } from "../../ui.js";
5
+ import { padOrTrimPlain, ellipsizeDisplay, sanitizeText } from "../rendering/render-text.js";
6
+ import { stringDisplayWidth } from "../../terminal-width.js";
7
+ import { RESUME_MENU_INITIAL_SESSION_ROWS, RESUME_MENU_LOAD_BATCH_ROWS, RESUME_MENU_LOAD_THRESHOLD_ROWS, RESUME_MENU_MAX_ROWS, SLASH_COMMAND_DESCRIPTION_COLUMN, SLASH_COMMAND_MENU_MAX_ROWS, THINKING_MENU_MAX_ROWS, } from "../constants.js";
8
+ import { APP_ICONS } from "../icons.js";
9
9
  const POPUP_MENU_ESCAPE_BUTTON = "Esc";
10
10
  export class AppPopupMenuController {
11
11
  host;
@@ -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,6 @@
1
- import { type PixConfig } from "../config.js";
2
- import type { Theme } from "../theme.js";
3
- import type { Entry, RenderedLine } from "./types.js";
1
+ import { type PixConfig } from "../../config.js";
2
+ import type { Theme } from "../../theme.js";
3
+ import type { Entry, RenderedLine } from "../types.js";
4
4
  export type InlineUserMessageMenuContext = {
5
5
  userContentWidth: number;
6
6
  userContentLeft: number;