pi-ui-extend 0.1.9 → 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 (92) hide show
  1. package/README.md +23 -2
  2. package/dist/app/app.d.ts +4 -0
  3. package/dist/app/app.js +74 -7
  4. package/dist/app/cli/install.d.ts +2 -0
  5. package/dist/app/cli/install.js +16 -1
  6. package/dist/app/commands/command-controller.js +4 -0
  7. package/dist/app/commands/command-host.d.ts +4 -0
  8. package/dist/app/commands/command-model-actions.d.ts +5 -0
  9. package/dist/app/commands/command-model-actions.js +104 -0
  10. package/dist/app/commands/command-navigation-actions.d.ts +6 -1
  11. package/dist/app/commands/command-navigation-actions.js +37 -14
  12. package/dist/app/commands/command-registry.d.ts +4 -0
  13. package/dist/app/commands/command-registry.js +32 -0
  14. package/dist/app/commands/command-session-actions.d.ts +1 -0
  15. package/dist/app/commands/command-session-actions.js +15 -5
  16. package/dist/app/commands/shell-controller.d.ts +1 -0
  17. package/dist/app/commands/shell-controller.js +1 -1
  18. package/dist/app/constants.d.ts +1 -1
  19. package/dist/app/constants.js +1 -1
  20. package/dist/app/icons.js +1 -1
  21. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  22. package/dist/app/input/autocomplete-controller.js +352 -0
  23. package/dist/app/input/input-action-controller.d.ts +1 -0
  24. package/dist/app/input/input-action-controller.js +21 -0
  25. package/dist/app/input/input-controller.d.ts +1 -0
  26. package/dist/app/input/input-controller.js +2 -0
  27. package/dist/app/input/input-paste-handler.d.ts +1 -0
  28. package/dist/app/input/input-paste-handler.js +22 -18
  29. package/dist/app/input/voice-controller.d.ts +2 -0
  30. package/dist/app/input/voice-controller.js +27 -15
  31. package/dist/app/model/model-usage-status.d.ts +9 -0
  32. package/dist/app/model/model-usage-status.js +124 -34
  33. package/dist/app/popup/popup-action-controller.js +1 -1
  34. package/dist/app/process.d.ts +17 -0
  35. package/dist/app/process.js +68 -0
  36. package/dist/app/rendering/conversation-entry-renderer.js +17 -6
  37. package/dist/app/rendering/conversation-tool-renderer.js +3 -2
  38. package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
  39. package/dist/app/rendering/editor-layout-renderer.js +11 -1
  40. package/dist/app/rendering/message-content.js +65 -7
  41. package/dist/app/rendering/render-controller.js +6 -1
  42. package/dist/app/rendering/render-text.d.ts +3 -0
  43. package/dist/app/rendering/render-text.js +51 -3
  44. package/dist/app/rendering/status-line-renderer.d.ts +5 -1
  45. package/dist/app/rendering/status-line-renderer.js +69 -25
  46. package/dist/app/rendering/tool-block-renderer.js +13 -31
  47. package/dist/app/runtime.d.ts +6 -1
  48. package/dist/app/runtime.js +35 -2
  49. package/dist/app/screen/clipboard.d.ts +2 -2
  50. package/dist/app/screen/clipboard.js +13 -18
  51. package/dist/app/screen/mouse-controller.d.ts +5 -2
  52. package/dist/app/screen/mouse-controller.js +16 -1
  53. package/dist/app/screen/screen-styler.d.ts +4 -1
  54. package/dist/app/screen/screen-styler.js +3 -2
  55. package/dist/app/screen/status-controller.d.ts +3 -0
  56. package/dist/app/screen/status-controller.js +23 -8
  57. package/dist/app/session/queued-message-controller.d.ts +7 -1
  58. package/dist/app/session/queued-message-controller.js +32 -21
  59. package/dist/app/session/resume-session-loader.d.ts +15 -0
  60. package/dist/app/session/resume-session-loader.js +204 -0
  61. package/dist/app/session/session-event-controller.d.ts +5 -1
  62. package/dist/app/session/session-event-controller.js +72 -5
  63. package/dist/app/session/session-history.js +4 -3
  64. package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
  65. package/dist/app/session/session-lifecycle-controller.js +9 -1
  66. package/dist/app/session/tabs-controller.d.ts +10 -1
  67. package/dist/app/session/tabs-controller.js +101 -5
  68. package/dist/app/terminal/nerd-font-controller.js +16 -17
  69. package/dist/app/terminal/terminal-controller.d.ts +1 -0
  70. package/dist/app/terminal/terminal-controller.js +1 -0
  71. package/dist/app/types.d.ts +14 -0
  72. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
  73. package/dist/app/workspace/workspace-actions-controller.js +3 -3
  74. package/dist/app/workspace/workspace-undo.d.ts +1 -1
  75. package/dist/app/workspace/workspace-undo.js +22 -20
  76. package/dist/config.d.ts +27 -0
  77. package/dist/config.js +174 -1
  78. package/dist/default-pix-config.js +38 -353
  79. package/dist/input-editor.d.ts +7 -1
  80. package/dist/input-editor.js +47 -6
  81. package/dist/markdown-format.d.ts +1 -0
  82. package/dist/markdown-format.js +26 -1
  83. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  84. package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
  85. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +45 -195
  86. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  87. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  88. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  89. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  90. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  91. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  92. package/package.json +1 -1
@@ -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,20 @@ 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 queuedMessagePrefix = (queuedEntry) => {
35
+ const label = queuedEntry.queueSource === "sdk-steering"
36
+ ? "steer"
37
+ : queuedEntry.queueSource === "sdk-follow-up"
38
+ ? "follow"
39
+ : "queued";
40
+ return `${APP_ICONS.timerSand} ${label}:`;
41
+ };
42
+ const queuedMessageLines = (queuedEntry) => {
43
+ const icon = APP_ICONS.timerSand;
44
+ const prefix = queuedMessagePrefix(queuedEntry);
45
+ const contentLines = wrapText(`${prefix} ${queuedEntry.text}`, userContentWidth);
46
+ return contentLines.map((text, index) => queuedLine(text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined));
47
+ };
37
48
  switch (entry.kind) {
38
49
  case "system":
39
50
  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) {
@@ -1,4 +1,10 @@
1
1
  import { isRecord } from "../guards.js";
2
+ const MAX_FORMAT_STRING_CHARS = 256 * 1024;
3
+ const MAX_RENDERED_CONTENT_CHARS = 512 * 1024;
4
+ const MAX_STRUCTURED_DEPTH = 8;
5
+ const MAX_STRUCTURED_ARRAY_ITEMS = 200;
6
+ const MAX_STRUCTURED_OBJECT_KEYS = 200;
7
+ const TRUNCATED_MARKER = "\n[… truncated …]";
2
8
  export function stringifyUnknown(value) {
3
9
  if (typeof value === "string")
4
10
  return value;
@@ -13,7 +19,7 @@ export function stringifyUnknown(value) {
13
19
  return name;
14
20
  }
15
21
  try {
16
- return JSON.stringify(value, null, 2);
22
+ return JSON.stringify(normalizeStructuredValue(value), null, 2);
17
23
  }
18
24
  catch {
19
25
  return String(value);
@@ -24,8 +30,10 @@ export function formatStructuredText(value) {
24
30
  const trimmed = value.trim();
25
31
  if (!trimmed)
26
32
  return "(empty)";
33
+ if (trimmed.length > MAX_FORMAT_STRING_CHARS)
34
+ return truncateText(value, MAX_FORMAT_STRING_CHARS);
27
35
  try {
28
- return JSON.stringify(JSON.parse(trimmed), null, 2);
36
+ return JSON.stringify(normalizeStructuredValue(JSON.parse(trimmed)), null, 2);
29
37
  }
30
38
  catch {
31
39
  return value;
@@ -36,25 +44,40 @@ export function formatStructuredText(value) {
36
44
  export function renderContent(content) {
37
45
  const parts = [];
38
46
  let imageCount = 0;
47
+ let renderedChars = 0;
48
+ const pushPart = (part) => {
49
+ const remaining = MAX_RENDERED_CONTENT_CHARS - renderedChars;
50
+ if (remaining <= 0)
51
+ return false;
52
+ const next = part.length > remaining ? truncateText(part, remaining) : part;
53
+ parts.push(next);
54
+ renderedChars += next.length;
55
+ return part.length <= remaining;
56
+ };
39
57
  for (const item of content) {
40
58
  if (!isRecord(item)) {
41
- parts.push(stringifyUnknown(item));
59
+ if (!pushPart(stringifyUnknown(item)))
60
+ break;
42
61
  continue;
43
62
  }
44
63
  if (isImageContent(item)) {
45
64
  imageCount += 1;
46
- parts.push(imageContentLabel(item, imageCount));
65
+ if (!pushPart(imageContentLabel(item, imageCount)))
66
+ break;
47
67
  continue;
48
68
  }
49
69
  if (typeof item.text === "string") {
50
- parts.push(item.text);
70
+ if (!pushPart(item.text))
71
+ break;
51
72
  continue;
52
73
  }
53
74
  if (typeof item.thinking === "string") {
54
- parts.push(item.thinking);
75
+ if (!pushPart(item.thinking))
76
+ break;
55
77
  continue;
56
78
  }
57
- parts.push(stringifyUnknown(item));
79
+ if (!pushPart(stringifyUnknown(item)))
80
+ break;
58
81
  }
59
82
  return parts.join("\n");
60
83
  }
@@ -113,3 +136,38 @@ export function submittedUserDisplayText(displayText, promptText, images) {
113
136
  return userImageLabels(images.length);
114
137
  return promptText.trimEnd();
115
138
  }
139
+ function truncateText(text, maxChars) {
140
+ if (text.length <= maxChars)
141
+ return text;
142
+ return `${text.slice(0, Math.max(0, maxChars))}${TRUNCATED_MARKER}`;
143
+ }
144
+ function normalizeStructuredValue(value, depth = 0, seen = new WeakSet()) {
145
+ if (typeof value === "string")
146
+ return truncateText(value, MAX_FORMAT_STRING_CHARS);
147
+ if (!value || typeof value !== "object")
148
+ return value;
149
+ if (depth >= MAX_STRUCTURED_DEPTH)
150
+ return "[… truncated: depth limit …]";
151
+ if (seen.has(value))
152
+ return "[… circular …]";
153
+ seen.add(value);
154
+ if (Array.isArray(value)) {
155
+ const items = value.slice(0, MAX_STRUCTURED_ARRAY_ITEMS).map((item) => normalizeStructuredValue(item, depth + 1, seen));
156
+ if (value.length > MAX_STRUCTURED_ARRAY_ITEMS)
157
+ items.push(`[… ${value.length - MAX_STRUCTURED_ARRAY_ITEMS} more items …]`);
158
+ return items;
159
+ }
160
+ if (value instanceof Error)
161
+ return value.message || value.name;
162
+ const output = {};
163
+ let count = 0;
164
+ for (const [key, child] of Object.entries(value)) {
165
+ if (count >= MAX_STRUCTURED_OBJECT_KEYS) {
166
+ output["…"] = "truncated: object key limit";
167
+ break;
168
+ }
169
+ output[key] = normalizeStructuredValue(child, depth + 1, seen);
170
+ count += 1;
171
+ }
172
+ return output;
173
+ }
@@ -68,8 +68,11 @@ export class AppRenderController {
68
68
  this.deps.mouseController.statusThinkingTarget = undefined;
69
69
  this.deps.mouseController.statusContextTarget = undefined;
70
70
  this.deps.mouseController.statusModelUsageTarget = undefined;
71
+ this.deps.mouseController.statusDraftQueueTarget = undefined;
72
+ this.deps.mouseController.statusUserJumpTarget = undefined;
71
73
  this.deps.mouseController.statusThinkingExpandTarget = undefined;
72
74
  this.deps.mouseController.statusCompactToolsTarget = undefined;
75
+ this.deps.mouseController.statusTerminalBellSoundTarget = undefined;
73
76
  this.deps.mouseController.statusSessionTarget = undefined;
74
77
  this.deps.mouseController.statusPromptEnhancerTarget = undefined;
75
78
  this.deps.mouseController.statusVoiceMicTarget = undefined;
@@ -165,10 +168,11 @@ export class AppRenderController {
165
168
  for (let index = 0; index < renderedInput.lines.length; index += 1) {
166
169
  const inputLine = renderedInput.lines[index] ?? "";
167
170
  const tagSpans = renderedInput.tagSpans[index];
171
+ const suggestionSpans = renderedInput.suggestionSpans?.[index] ?? [];
168
172
  const row = toScreenRow(inputStartRow + index);
169
173
  this.deps.mouseController.renderedRowTexts.set(row, inputLine);
170
174
  const tagColor = this.deps.theme.colors.accent;
171
- const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, columns, tagColor, this.deps.theme.colors.inputBorder);
175
+ const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted, this.deps.theme.colors.inputBorder);
172
176
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, styledLine));
173
177
  }
174
178
  if (renderedInput.scrollBar && columns > 0) {
@@ -291,6 +295,7 @@ export class AppRenderController {
291
295
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
292
296
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
293
297
  this.deps.mouseController.statusModelUsageTarget = this.deps.statusLineRenderer.modelUsageTarget(statusLayout.text, statusRow, statusLayout);
298
+ this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(statusLayout, statusRow);
294
299
  this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(statusLayout, statusRow);
295
300
  this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(statusLayout, statusRow);
296
301
  this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(statusLayout, statusRow);
@@ -1,10 +1,13 @@
1
1
  import type { Theme } from "../../theme.js";
2
2
  import type { ToolStatusEntry } from "../types.js";
3
3
  export declare function sanitizeText(text: string): string;
4
+ export declare function alertIconPrefixLength(text: string): number | undefined;
4
5
  export declare function normalizePastedTextForDuplicateKey(text: string): string;
5
6
  export declare function shortHash(text: string): string;
6
7
  export declare function hasLspDiagnosticsAfterMutation(output: string): boolean;
7
8
  export declare function hasToolLspDiagnosticsAfterMutation(entry: ToolStatusEntry): boolean;
9
+ export declare function lspDiagnosticSeverityForLine(line: string): "error" | "warning" | "hint" | undefined;
10
+ export declare function toolLspDiagnosticsAfterMutationSeverity(entry: ToolStatusEntry): "error" | "warning" | undefined;
8
11
  export declare function toolStatusIcon(entry: ToolStatusEntry): string;
9
12
  export declare function toolStatusIconColor(entry: ToolStatusEntry, colors: Theme["colors"]): string;
10
13
  export declare function wrapLine(text: string, width: number): string[];