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
@@ -7,7 +7,7 @@ import { isRecord } from "../guards.js";
7
7
  import { createId } from "../id.js";
8
8
  import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-info.js";
9
9
  import { tabPanelRows } from "../rendering/tab-line-renderer.js";
10
- const TAB_STATE_VERSION = 2;
10
+ const TAB_STATE_VERSION = 3;
11
11
  const MAX_RESTORED_TABS = 8;
12
12
  const TAB_ATTENTION_BLINK_KEY = "tab-attention";
13
13
  export class AppTabsController {
@@ -16,6 +16,7 @@ export class AppTabsController {
16
16
  runtimesByTabId = new Map();
17
17
  runtimeSubscriptionsByTabId = new Map();
18
18
  inputStatesByTabId = new Map();
19
+ deferredUserMessagesByTabId = new Map();
19
20
  activeTabId;
20
21
  pendingActiveTabId;
21
22
  historyLoadGeneration = 0;
@@ -96,8 +97,13 @@ export class AppTabsController {
96
97
  async saveInputStateForQuit() {
97
98
  this.syncActiveTabFromRuntime({ save: false });
98
99
  this.storeActiveInputState();
100
+ this.storeActiveDeferredUserMessages();
99
101
  await this.saveTabs();
100
102
  }
103
+ persistActiveDeferredUserMessages() {
104
+ this.storeActiveDeferredUserMessages();
105
+ void this.saveTabs();
106
+ }
101
107
  syncActiveTabFromRuntime(options = {}) {
102
108
  if (this.pendingActiveTabId && options.force !== true)
103
109
  return;
@@ -108,14 +114,17 @@ export class AppTabsController {
108
114
  const active = this.activeTab();
109
115
  const existing = sessionPath ? this.findTabBySessionPath(sessionPath, active ? { excludeTabId: active.id } : {}) : undefined;
110
116
  if (existing) {
111
- if (active)
117
+ if (active) {
112
118
  this.storeActiveInputState();
119
+ this.storeActiveDeferredUserMessages();
120
+ }
113
121
  this.activeTabId = existing.id;
114
122
  this.clearTabAttention(existing);
115
123
  this.updateTabFromSession(existing, session);
116
124
  if (active)
117
125
  this.deleteRuntimeForTab(active.id);
118
126
  this.storeActiveRuntime();
127
+ this.restoreDeferredUserMessages(existing.id);
119
128
  if (options.save !== false)
120
129
  void this.saveTabs();
121
130
  return;
@@ -167,6 +176,7 @@ export class AppTabsController {
167
176
  : restoredTabs[0]?.sessionPath;
168
177
  this.replaceTabs(restoredTabs, desiredPath);
169
178
  this.restorePersistedInputStates(saved);
179
+ this.restorePersistedDeferredUserMessages(saved);
170
180
  if (explicitSessionPath && currentPath)
171
181
  this.ensureCurrentSessionTab(runtime.session);
172
182
  if (!desiredPath) {
@@ -191,6 +201,8 @@ export class AppTabsController {
191
201
  }
192
202
  this.syncActiveTabFromRuntime({ save: false });
193
203
  this.host.resetSessionView();
204
+ if (this.activeTabId)
205
+ this.restoreDeferredUserMessages(this.activeTabId);
194
206
  this.host.loadSessionHistory();
195
207
  this.host.setSessionStatus(runtime.session);
196
208
  this.host.setSessionActivity(this.sessionActivity(runtime.session));
@@ -211,6 +223,7 @@ export class AppTabsController {
211
223
  this.cancelHistoryLoad();
212
224
  this.syncActiveTabFromRuntime();
213
225
  this.storeActiveInputState();
226
+ this.storeActiveDeferredUserMessages();
214
227
  this.host.setStatus("starting new tab");
215
228
  this.host.render();
216
229
  const newRuntime = await this.host.createRuntimeForNewSession();
@@ -233,6 +246,7 @@ export class AppTabsController {
233
246
  }
234
247
  void this.saveTabs();
235
248
  this.host.resetSessionView();
249
+ this.restoreDeferredUserMessages(tab.id);
236
250
  if (isEmptyStartupSession(newRuntime)) {
237
251
  this.host.addEntry({ id: createId("system"), kind: "system", text: createStartupInfoMessage(newRuntime) });
238
252
  }
@@ -270,6 +284,7 @@ export class AppTabsController {
270
284
  }
271
285
  this.cancelHistoryLoad();
272
286
  this.storeActiveInputState();
287
+ this.storeActiveDeferredUserMessages();
273
288
  const previousTabId = this.activeTabId;
274
289
  const previousRuntime = runtime;
275
290
  this.host.setStatus("opening session tab");
@@ -312,6 +327,8 @@ export class AppTabsController {
312
327
  void this.host.disposeRuntime(newRuntime);
313
328
  this.host.showToast("Could not open session tab", "warning");
314
329
  this.host.resetSessionView();
330
+ if (previousTabId)
331
+ this.restoreDeferredUserMessages(previousTabId);
315
332
  this.host.loadSessionHistory();
316
333
  this.host.setSessionStatus(this.host.runtime()?.session);
317
334
  this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
@@ -353,12 +370,14 @@ export class AppTabsController {
353
370
  const previousTargetActivity = target.activity;
354
371
  this.storeActiveRuntime(runtime);
355
372
  this.storeActiveInputState();
373
+ this.storeActiveDeferredUserMessages();
356
374
  this.activeTabId = target.id;
357
375
  this.pendingActiveTabId = target.id;
358
376
  target.activity = "thinking";
359
377
  this.clearTabAttention(target);
360
378
  this.restoreInputState(target.id);
361
379
  this.host.resetSessionView();
380
+ this.restoreDeferredUserMessages(target.id);
362
381
  this.host.setStatus("switching tab");
363
382
  this.host.setSessionActivity("thinking");
364
383
  this.host.render();
@@ -388,6 +407,8 @@ export class AppTabsController {
388
407
  }
389
408
  this.host.showToast("Could not switch tab", "warning");
390
409
  this.host.resetSessionView();
410
+ if (previousTabId)
411
+ this.restoreDeferredUserMessages(previousTabId);
391
412
  this.host.loadSessionHistory();
392
413
  const activeSession = this.host.runtime()?.session;
393
414
  this.host.setSessionStatus(activeSession);
@@ -426,7 +447,9 @@ export class AppTabsController {
426
447
  this.tabItems.splice(index, 1);
427
448
  this.deleteRuntimeForTab(tabId);
428
449
  this.inputStatesByTabId.delete(tabId);
450
+ this.deferredUserMessagesByTabId.delete(tabId);
429
451
  this.storeActiveInputState();
452
+ this.storeActiveDeferredUserMessages();
430
453
  this.stopAttentionBlinkIfIdle();
431
454
  if (tabRuntime)
432
455
  void this.host.disposeRuntime(tabRuntime);
@@ -451,6 +474,7 @@ export class AppTabsController {
451
474
  this.tabItems.splice(index, 1);
452
475
  this.deleteRuntimeForTab(tabId);
453
476
  this.inputStatesByTabId.delete(tabId);
477
+ this.deferredUserMessagesByTabId.delete(tabId);
454
478
  this.stopAttentionBlinkIfIdle();
455
479
  this.activeTabId = nextTab.id;
456
480
  this.clearTabAttention(nextTab);
@@ -483,9 +507,11 @@ export class AppTabsController {
483
507
  this.updateTabFromSession(tab, runtime.session);
484
508
  this.setRuntimeForTab(tab.id, runtime);
485
509
  this.inputStatesByTabId.delete(tab.id);
510
+ this.deferredUserMessagesByTabId.delete(tab.id);
486
511
  this.restoreInputState(tab.id);
487
512
  this.stopAttentionBlinkIfIdle();
488
513
  this.host.resetSessionView();
514
+ this.restoreDeferredUserMessages(tab.id);
489
515
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Started a new session. cwd=${runtime.cwd}` });
490
516
  if (runtime.modelFallbackMessage)
491
517
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
@@ -502,6 +528,8 @@ export class AppTabsController {
502
528
  const generation = ++this.historyLoadGeneration;
503
529
  const isCancelled = () => generation !== this.historyLoadGeneration || this.host.runtime() !== runtime;
504
530
  this.host.resetSessionView();
531
+ if (this.activeTabId)
532
+ this.restoreDeferredUserMessages(this.activeTabId);
505
533
  this.host.setStatus("loading session history");
506
534
  this.host.setSessionActivity("thinking");
507
535
  this.host.render();
@@ -620,9 +648,31 @@ export class AppTabsController {
620
648
  cursor: state.cursor,
621
649
  });
622
650
  }
651
+ storeActiveDeferredUserMessages() {
652
+ if (!this.activeTabId || !this.host.captureDeferredUserMessages)
653
+ return;
654
+ const messages = this.host.captureDeferredUserMessages();
655
+ if (messages.length > 0) {
656
+ this.deferredUserMessagesByTabId.set(this.activeTabId, messages.map((message) => this.cloneSubmittedUserMessage(message)));
657
+ }
658
+ else {
659
+ this.deferredUserMessagesByTabId.delete(this.activeTabId);
660
+ }
661
+ }
623
662
  restoreInputState(tabId) {
624
663
  this.host.restoreInputState(this.inputStatesByTabId.get(tabId) ?? { text: "", cursor: 0 });
625
664
  }
665
+ restoreDeferredUserMessages(tabId) {
666
+ this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
667
+ }
668
+ cloneSubmittedUserMessage(message) {
669
+ return {
670
+ id: message.id,
671
+ promptText: message.promptText,
672
+ displayText: message.displayText,
673
+ images: message.images.map((image) => ({ ...image })),
674
+ };
675
+ }
626
676
  async runtimeForTab(tab) {
627
677
  const existing = this.runtimesByTabId.get(tab.id);
628
678
  if (existing)
@@ -650,6 +700,7 @@ export class AppTabsController {
650
700
  this.runtimesByTabId.clear();
651
701
  this.clearRuntimeSubscriptions();
652
702
  this.inputStatesByTabId.clear();
703
+ this.deferredUserMessagesByTabId.clear();
653
704
  const seen = new Set();
654
705
  for (const tab of tabs) {
655
706
  const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
@@ -678,6 +729,7 @@ export class AppTabsController {
678
729
  this.tabItems.splice(index, 1);
679
730
  this.deleteRuntimeForTab(tabId);
680
731
  this.inputStatesByTabId.delete(tabId);
732
+ this.deferredUserMessagesByTabId.delete(tabId);
681
733
  }
682
734
  restorePersistedInputStates(saved) {
683
735
  const inputsByPath = new Map();
@@ -695,6 +747,22 @@ export class AppTabsController {
695
747
  this.inputStatesByTabId.set(tab.id, input);
696
748
  }
697
749
  }
750
+ restorePersistedDeferredUserMessages(saved) {
751
+ const messagesByPath = new Map();
752
+ for (const tab of saved.tabs) {
753
+ if (!tab.deferredUserMessages || tab.deferredUserMessages.length === 0)
754
+ continue;
755
+ messagesByPath.set(resolve(tab.path), tab.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
756
+ }
757
+ for (const tab of this.tabItems) {
758
+ if (!tab.sessionPath)
759
+ continue;
760
+ const messages = messagesByPath.get(resolve(tab.sessionPath));
761
+ if (!messages || messages.length === 0)
762
+ continue;
763
+ this.deferredUserMessagesByTabId.set(tab.id, messages);
764
+ }
765
+ }
698
766
  ensureCurrentSessionTab(session) {
699
767
  const currentPath = session.sessionFile ? resolve(session.sessionFile) : undefined;
700
768
  if (!currentPath)
@@ -783,7 +851,8 @@ export class AppTabsController {
783
851
  for (const tab of saved.tabs) {
784
852
  const sessionPath = resolve(tab.path);
785
853
  const hasDraftInput = (tab.input?.text.length ?? 0) > 0;
786
- if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput))
854
+ const hasDeferredQueue = (tab.deferredUserMessages?.length ?? 0) > 0;
855
+ if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasDeferredQueue))
787
856
  continue;
788
857
  seen.add(sessionPath);
789
858
  const title = titles.get(sessionPath) ?? tab.title?.trim();
@@ -802,21 +871,23 @@ export class AppTabsController {
802
871
  try {
803
872
  const raw = await readFile(this.filePath(), "utf8");
804
873
  const parsed = JSON.parse(raw);
805
- if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== TAB_STATE_VERSION) || !Array.isArray(parsed.tabs))
874
+ if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== 2 && parsed.version !== TAB_STATE_VERSION) || !Array.isArray(parsed.tabs))
806
875
  return undefined;
807
876
  const tabs = [];
808
877
  for (const value of parsed.tabs) {
809
878
  if (!isRecord(value) || typeof value.path !== "string")
810
879
  continue;
811
880
  const input = this.parsePersistedInputState(value.input);
881
+ const deferredUserMessages = this.parsePersistedSubmittedUserMessages(value.deferredUserMessages);
812
882
  tabs.push({
813
883
  path: value.path,
814
884
  ...(typeof value.title === "string" ? { title: value.title } : {}),
815
885
  ...(input ? { input } : {}),
886
+ ...(deferredUserMessages.length > 0 ? { deferredUserMessages } : {}),
816
887
  });
817
888
  }
818
889
  return {
819
- version: parsed.version === 1 ? 1 : TAB_STATE_VERSION,
890
+ version: parsed.version === 1 ? 1 : parsed.version === 2 ? 2 : TAB_STATE_VERSION,
820
891
  cwd: typeof parsed.cwd === "string" ? parsed.cwd : this.host.options.cwd,
821
892
  tabs,
822
893
  ...(typeof parsed.activePath === "string" ? { activePath: parsed.activePath } : {}),
@@ -836,6 +907,27 @@ export class AppTabsController {
836
907
  : value.text.length;
837
908
  return { text: value.text, cursor };
838
909
  }
910
+ parsePersistedSubmittedUserMessages(value) {
911
+ if (!Array.isArray(value))
912
+ return [];
913
+ const messages = [];
914
+ for (const item of value) {
915
+ if (!isRecord(item) || typeof item.promptText !== "string" || typeof item.displayText !== "string")
916
+ continue;
917
+ const images = Array.isArray(item.images)
918
+ ? item.images.flatMap((image) => (isRecord(image) && typeof image.data === "string" && typeof image.mimeType === "string"
919
+ ? [{ type: "image", data: image.data, mimeType: image.mimeType }]
920
+ : []))
921
+ : [];
922
+ messages.push({
923
+ id: typeof item.id === "string" ? item.id : createId("queued-user"),
924
+ promptText: item.promptText,
925
+ displayText: item.displayText,
926
+ images,
927
+ });
928
+ }
929
+ return messages;
930
+ }
839
931
  async saveTabs() {
840
932
  if (this.host.options.noSession)
841
933
  return;
@@ -857,6 +949,10 @@ export class AppTabsController {
857
949
  cursor: Math.max(0, Math.min(input.text.length, Math.trunc(input.cursor))),
858
950
  };
859
951
  }
952
+ const deferredUserMessages = this.deferredUserMessagesByTabId.get(tab.id);
953
+ if (deferredUserMessages && deferredUserMessages.length > 0) {
954
+ persistedTab.deferredUserMessages = deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
955
+ }
860
956
  tabs.push(persistedTab);
861
957
  }
862
958
  if (tabs.length === 0)
@@ -1,3 +1,18 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
4
+ import { commandExists, runProcess } from "../process.js";
5
+ type NerdFontControllerDeps = {
6
+ commandExists: typeof commandExists;
7
+ existsSync: typeof existsSync;
8
+ fetch: typeof fetch;
9
+ mkdir: typeof mkdir;
10
+ readdir: typeof readdir;
11
+ runProcess: typeof runProcess;
12
+ spawn: typeof spawn;
13
+ writeFile: typeof writeFile;
14
+ };
15
+ export declare function setNerdFontControllerTestDeps(overrides: Partial<NerdFontControllerDeps>): () => void;
1
16
  export type NerdFontInstallHost = {
2
17
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
3
18
  render(): void;
@@ -15,3 +30,4 @@ export declare class NerdFontController {
15
30
  export declare function isJetBrainsNerdFontInstalled(): Promise<boolean>;
16
31
  export declare function installJetBrainsNerdFont(): Promise<string>;
17
32
  export declare function userFontInstallPath(): string;
33
+ export {};
@@ -1,8 +1,17 @@
1
- import { spawn, spawnSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { mkdir, readdir, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
+ import { commandExists, runProcess } from "../process.js";
7
+ let deps = { commandExists, existsSync, fetch, mkdir, readdir, runProcess, spawn, writeFile };
8
+ export function setNerdFontControllerTestDeps(overrides) {
9
+ const previous = deps;
10
+ deps = { ...deps, ...overrides };
11
+ return () => {
12
+ deps = previous;
13
+ };
14
+ }
6
15
  const CASK_NAME = "font-jetbrains-mono-nerd-font";
7
16
  export const FONT_FAMILY_NAME = "JetBrainsMono Nerd Font Mono";
8
17
  export const FONT_FILE_NAME = "JetBrainsMonoNerdFontMono-Regular.ttf";
@@ -42,10 +51,13 @@ export class NerdFontController {
42
51
  }
43
52
  }
44
53
  export async function isJetBrainsNerdFontInstalled() {
45
- if (commandExists("brew") && spawnSync("brew", ["list", "--cask", CASK_NAME], { stdio: "ignore" }).status === 0)
46
- return true;
47
- if (process.platform === "linux" && commandExists("fc-match")) {
48
- const result = spawnSync("fc-match", ["-f", "%{family}", FONT_FAMILY_NAME], { encoding: "utf8" });
54
+ if (await deps.commandExists("brew")) {
55
+ const result = await deps.runProcess("brew", ["list", "--cask", CASK_NAME], { maxBufferBytes: 1024 });
56
+ if (result.status === 0)
57
+ return true;
58
+ }
59
+ if (process.platform === "linux" && await deps.commandExists("fc-match")) {
60
+ const result = await deps.runProcess("fc-match", ["-f", "%{family}", FONT_FAMILY_NAME], { maxBufferBytes: 1024 });
49
61
  if (result.status === 0 && /JetBrains.*Nerd/iu.test(result.stdout))
50
62
  return true;
51
63
  }
@@ -57,13 +69,13 @@ export async function isJetBrainsNerdFontInstalled() {
57
69
  return false;
58
70
  }
59
71
  export async function installJetBrainsNerdFont() {
60
- if (process.platform === "darwin" && commandExists("brew")) {
72
+ if (process.platform === "darwin" && await deps.commandExists("brew")) {
61
73
  await runBrewInstall();
62
74
  return CASK_NAME;
63
75
  }
64
76
  const targetPath = userFontInstallPath();
65
- await mkdir(dirname(targetPath), { recursive: true });
66
- const response = await fetch(FONT_DOWNLOAD_URL, {
77
+ await deps.mkdir(dirname(targetPath), { recursive: true });
78
+ const response = await deps.fetch(FONT_DOWNLOAD_URL, {
67
79
  headers: { "User-Agent": "pix-font-installer" },
68
80
  signal: AbortSignal.timeout(30_000),
69
81
  });
@@ -72,11 +84,11 @@ export async function installJetBrainsNerdFont() {
72
84
  const bytes = new Uint8Array(await response.arrayBuffer());
73
85
  if (bytes.length < 100_000)
74
86
  throw new Error("downloaded font is unexpectedly small");
75
- await writeFile(targetPath, bytes);
87
+ await deps.writeFile(targetPath, bytes);
76
88
  if (process.platform === "linux")
77
- runOptionalCommand("fc-cache", ["-f", dirname(targetPath)]);
89
+ await runOptionalCommand("fc-cache", ["-f", dirname(targetPath)]);
78
90
  if (process.platform === "win32")
79
- registerWindowsUserFont(targetPath);
91
+ await registerWindowsUserFont(targetPath);
80
92
  return targetPath;
81
93
  }
82
94
  export function userFontInstallPath() {
@@ -103,7 +115,7 @@ function platformFontDirs() {
103
115
  }
104
116
  }
105
117
  async function directoryContainsFont(root) {
106
- if (!existsSync(root))
118
+ if (!deps.existsSync(root))
107
119
  return false;
108
120
  const pending = [{ dir: root, depth: 0 }];
109
121
  let scanned = 0;
@@ -113,7 +125,7 @@ async function directoryContainsFont(root) {
113
125
  continue;
114
126
  let entries;
115
127
  try {
116
- entries = await readdir(current.dir, { withFileTypes: true });
128
+ entries = await deps.readdir(current.dir, { withFileTypes: true });
117
129
  }
118
130
  catch {
119
131
  continue;
@@ -130,7 +142,7 @@ async function directoryContainsFont(root) {
130
142
  }
131
143
  async function runBrewInstall() {
132
144
  await new Promise((resolve, reject) => {
133
- const child = spawn("brew", ["install", "--cask", CASK_NAME], {
145
+ const child = deps.spawn("brew", ["install", "--cask", CASK_NAME], {
134
146
  env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" },
135
147
  stdio: ["ignore", "ignore", "pipe"],
136
148
  });
@@ -147,10 +159,10 @@ async function runBrewInstall() {
147
159
  });
148
160
  });
149
161
  }
150
- function registerWindowsUserFont(fontPath) {
162
+ async function registerWindowsUserFont(fontPath) {
151
163
  const escapedPath = fontPath.replaceAll("'", "''");
152
164
  const escapedName = `${FONT_FAMILY_NAME} (TrueType)`.replaceAll("'", "''");
153
- runOptionalCommand("powershell.exe", [
165
+ await runOptionalCommand("powershell.exe", [
154
166
  "-NoProfile",
155
167
  "-ExecutionPolicy",
156
168
  "Bypass",
@@ -158,13 +170,8 @@ function registerWindowsUserFont(fontPath) {
158
170
  `New-Item -Path 'HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' -Force | Out-Null; New-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' -Name '${escapedName}' -Value '${escapedPath}' -PropertyType String -Force | Out-Null`,
159
171
  ]);
160
172
  }
161
- function runOptionalCommand(command, args) {
162
- spawnSync(command, args, { stdio: "ignore" });
163
- }
164
- function commandExists(command) {
165
- if (process.platform === "win32")
166
- return spawnSync("where", [command], { stdio: "ignore" }).status === 0;
167
- return spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" }).status === 0;
173
+ async function runOptionalCommand(command, args) {
174
+ await deps.runProcess(command, args, { maxBufferBytes: 1024 });
168
175
  }
169
176
  function errorMessage(error) {
170
177
  return error instanceof Error ? error.message : String(error);
@@ -13,6 +13,7 @@ export type AppTerminalControllerHost = {
13
13
  stopSubagentsPolling(): void;
14
14
  stopModelUsagePolling(): void;
15
15
  stopVoiceInput(): Promise<void>;
16
+ stopAutocomplete(): void;
16
17
  stopShellCommand(): void;
17
18
  unsubscribeSession(): void;
18
19
  clearExtensionWidgets(): void;
@@ -104,6 +104,7 @@ export class AppTerminalController {
104
104
  this.host.stopSubagentsPolling();
105
105
  this.host.stopModelUsagePolling();
106
106
  await this.host.stopVoiceInput();
107
+ this.host.stopAutocomplete();
107
108
  this.host.stopShellCommand();
108
109
  process.stdin.off("data", this.onInputData);
109
110
  process.stdin.pause();
@@ -264,6 +264,7 @@ export type StatusLineLayout = {
264
264
  modelUsageLabel?: string;
265
265
  contextBarLabel?: string;
266
266
  userJumpWidget?: StatusUserJumpWidgetLayout;
267
+ draftQueueWidget?: StatusDraftQueueWidgetLayout;
267
268
  thinkingExpandWidget?: StatusThinkingExpandWidgetLayout;
268
269
  compactToolsWidget?: StatusCompactToolsWidgetLayout;
269
270
  terminalBellSoundWidget?: StatusTerminalBellSoundWidgetLayout;
@@ -274,6 +275,10 @@ export type StatusUserJumpWidgetLayout = {
274
275
  startColumn: number;
275
276
  endColumn: number;
276
277
  };
278
+ export type StatusDraftQueueWidgetLayout = {
279
+ startColumn: number;
280
+ endColumn: number;
281
+ };
277
282
  export type StatusThinkingExpandWidgetLayout = {
278
283
  startColumn: number;
279
284
  endColumn: number;
@@ -357,6 +362,10 @@ export type RenderedInput = {
357
362
  start: number;
358
363
  end: number;
359
364
  }[])[];
365
+ suggestionSpans: readonly (readonly {
366
+ start: number;
367
+ end: number;
368
+ }[])[];
360
369
  };
361
370
  export type ScrollBarMetrics = {
362
371
  top: number;
@@ -547,6 +556,11 @@ export type StatusUserJumpTarget = {
547
556
  startColumn: number;
548
557
  endColumn: number;
549
558
  };
559
+ export type StatusDraftQueueTarget = {
560
+ row: number;
561
+ startColumn: number;
562
+ endColumn: number;
563
+ };
550
564
  export type StatusThinkingExpandTarget = {
551
565
  row: number;
552
566
  startColumn: number;
@@ -28,7 +28,7 @@ export declare class AppWorkspaceActionsController {
28
28
  recordWorkspaceMutationForUserEntry(entryId: string, mutation: WorkspaceMutation): void;
29
29
  scheduleUserSessionEntryMetadataSync(): void;
30
30
  syncUserSessionEntryMetadata(): void;
31
- copyUserMessage(entryId: string): void;
31
+ copyUserMessage(entryId: string): Promise<void>;
32
32
  forkFromUserMessage(entryId: string): Promise<void>;
33
33
  undoChangesFromUserMessage(entryId: string): Promise<void>;
34
34
  private resolveUserSessionEntryId;
@@ -74,11 +74,11 @@ export class AppWorkspaceActionsController {
74
74
  this.host.touchEntry(entry);
75
75
  }
76
76
  }
77
- copyUserMessage(entryId) {
77
+ async copyUserMessage(entryId) {
78
78
  const entry = this.host.findUserEntry(entryId);
79
79
  if (!entry)
80
80
  throw new Error("User message is no longer available");
81
- copyTextToClipboard(entry.text);
81
+ await copyTextToClipboard(entry.text);
82
82
  this.host.showToast("Message copied", "success");
83
83
  this.host.setSessionStatus(this.host.runtime()?.session);
84
84
  }
@@ -127,7 +127,7 @@ export class AppWorkspaceActionsController {
127
127
  this.host.setStatus("reverting recorded commands");
128
128
  this.host.render();
129
129
  }
130
- const reverted = mutations.length === 0 ? { ok: true, changedFiles: 0, revertedChanges: 0 } : revertWorkspaceMutations(runtime.cwd, mutations);
130
+ const reverted = mutations.length === 0 ? { ok: true, changedFiles: 0, revertedChanges: 0 } : await revertWorkspaceMutations(runtime.cwd, mutations);
131
131
  if (!reverted.ok)
132
132
  throw new Error(reverted.error);
133
133
  this.host.setStatus("truncating session");
@@ -41,4 +41,4 @@ export declare function loadWorkspaceUndoIndex(agentDir: string): WorkspaceUndoI
41
41
  export declare function saveWorkspaceUndoIndex(agentDir: string, index: WorkspaceUndoIndex): void;
42
42
  export declare function prepareWorkspaceMutation(cwd: string, toolName: string, args: unknown): WorkspaceMutationPreparation | undefined;
43
43
  export declare function workspaceMutationFromToolExecution(input: WorkspaceMutationFromToolInput): WorkspaceMutation | undefined;
44
- export declare function revertWorkspaceMutations(cwd: string, mutations: readonly WorkspaceMutation[]): WorkspaceRevertResult;
44
+ export declare function revertWorkspaceMutations(cwd: string, mutations: readonly WorkspaceMutation[]): Promise<WorkspaceRevertResult>;
@@ -1,6 +1,7 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { runProcess } from "../process.js";
4
5
  const UNDO_INDEX_VERSION = 1;
5
6
  export function workspaceUndoIndexKey(_sessionFile, _sessionId, entryId) {
6
7
  return entryId;
@@ -63,13 +64,13 @@ export function workspaceMutationFromToolExecution(input) {
63
64
  }
64
65
  return undefined;
65
66
  }
66
- export function revertWorkspaceMutations(cwd, mutations) {
67
+ export async function revertWorkspaceMutations(cwd, mutations) {
67
68
  const changedFiles = new Set();
68
69
  const applied = [];
69
70
  for (const mutation of [...mutations].reverse()) {
70
- const result = applyMutation(cwd, mutation, "undo");
71
+ const result = await applyMutation(cwd, mutation, "undo");
71
72
  if (!result.ok) {
72
- const rollback = rollbackMutations(cwd, applied);
73
+ const rollback = await rollbackMutations(cwd, applied);
73
74
  const rollbackText = rollback.ok ? "Rolled back already-applied undo steps." : `Rollback failed: ${rollback.error}`;
74
75
  return { ok: false, error: `${result.error}\n${rollbackText}` };
75
76
  }
@@ -79,30 +80,30 @@ export function revertWorkspaceMutations(cwd, mutations) {
79
80
  }
80
81
  return { ok: true, changedFiles: changedFiles.size, revertedChanges: applied.length };
81
82
  }
82
- function rollbackMutations(cwd, appliedUndoMutations) {
83
+ async function rollbackMutations(cwd, appliedUndoMutations) {
83
84
  for (const mutation of [...appliedUndoMutations].reverse()) {
84
- const result = applyMutation(cwd, mutation, "redo");
85
+ const result = await applyMutation(cwd, mutation, "redo");
85
86
  if (!result.ok)
86
87
  return { ok: false, error: result.error };
87
88
  }
88
89
  return { ok: true, changedFiles: 0, revertedChanges: appliedUndoMutations.length };
89
90
  }
90
- function applyMutation(cwd, mutation, direction) {
91
+ async function applyMutation(cwd, mutation, direction) {
91
92
  if (mutation.type === "patch")
92
93
  return applyPatchMutation(cwd, mutation, direction);
93
94
  return applyWriteMutation(cwd, mutation, direction);
94
95
  }
95
- function applyPatchMutation(cwd, mutation, direction) {
96
+ async function applyPatchMutation(cwd, mutation, direction) {
96
97
  const args = ["apply", ...(direction === "undo" ? ["--reverse"] : []), "--whitespace=nowarn"];
97
- const check = runGitApply(cwd, [...args, "--check"], mutation.patch);
98
+ const check = await runGitApply(cwd, [...args, "--check"], mutation.patch);
98
99
  if (check.status !== 0)
99
100
  return { ok: false, error: commandError(`git ${args.join(" ")} --check`, check) };
100
- const apply = runGitApply(cwd, args, mutation.patch);
101
+ const apply = await runGitApply(cwd, args, mutation.patch);
101
102
  if (apply.status !== 0)
102
103
  return { ok: false, error: commandError(`git ${args.join(" ")}`, apply) };
103
104
  return { ok: true, changedFiles: filesFromPatch(mutation.patch) };
104
105
  }
105
- function applyWriteMutation(cwd, mutation, direction) {
106
+ async function applyWriteMutation(cwd, mutation, direction) {
106
107
  const safePath = safeRelativePath(cwd, mutation.path);
107
108
  if (!safePath)
108
109
  return { ok: false, error: `Refusing to modify path outside workspace: ${mutation.path}` };
@@ -110,17 +111,17 @@ function applyWriteMutation(cwd, mutation, direction) {
110
111
  const expectedContent = direction === "undo" ? mutation.afterContent : mutation.beforeContent;
111
112
  const nextContent = direction === "undo" ? mutation.beforeContent : mutation.afterContent;
112
113
  const currentExists = existsSync(absolutePath);
113
- const currentContent = currentExists ? readFileSync(absolutePath, "utf8") : undefined;
114
+ const currentContent = currentExists ? await readFile(absolutePath, "utf8") : undefined;
114
115
  if (currentContent !== expectedContent) {
115
116
  return { ok: false, error: `Refusing to ${direction} write for ${safePath}: file content changed since the recorded command.` };
116
117
  }
117
118
  if (nextContent === undefined) {
118
119
  if (currentExists)
119
- rmSync(absolutePath, { force: true });
120
+ await rm(absolutePath, { force: true });
120
121
  }
121
122
  else {
122
- mkdirSync(dirname(absolutePath), { recursive: true });
123
- writeFileSync(absolutePath, nextContent, "utf8");
123
+ await mkdir(dirname(absolutePath), { recursive: true });
124
+ await writeFile(absolutePath, nextContent, "utf8");
124
125
  }
125
126
  return { ok: true, changedFiles: [safePath] };
126
127
  }
@@ -138,17 +139,18 @@ function parseUndoIndex(value) {
138
139
  function workspaceUndoIndexPath(agentDir) {
139
140
  return join(agentDir, "pix", "workspace-undo", "index.json");
140
141
  }
141
- function runGitApply(cwd, args, input) {
142
- return spawnSync("git", ["-c", "core.autocrlf=false", ...args], {
142
+ async function runGitApply(cwd, args, input) {
143
+ return runProcess("git", ["-c", "core.autocrlf=false", ...args], {
143
144
  cwd,
144
145
  input,
145
- encoding: "utf8",
146
- maxBuffer: 20 * 1024 * 1024,
146
+ maxBufferBytes: 20 * 1024 * 1024,
147
147
  });
148
148
  }
149
149
  function commandError(command, result) {
150
150
  if (result.error)
151
151
  return `${command} failed: ${result.error.message}`;
152
+ if (result.timedOut)
153
+ return `${command} timed out`;
152
154
  const message = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status ?? "unknown"}`;
153
155
  return `${command} failed: ${message}`;
154
156
  }