@mariozechner/pi-coding-agent 0.42.1 → 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -1
  3. package/dist/core/extensions/types.d.ts +8 -2
  4. package/dist/core/extensions/types.d.ts.map +1 -1
  5. package/dist/core/extensions/types.js.map +1 -1
  6. package/dist/core/footer-data-provider.d.ts +25 -0
  7. package/dist/core/footer-data-provider.d.ts.map +1 -0
  8. package/dist/core/footer-data-provider.js +115 -0
  9. package/dist/core/footer-data-provider.js.map +1 -0
  10. package/dist/core/keybindings.d.ts +1 -1
  11. package/dist/core/keybindings.d.ts.map +1 -1
  12. package/dist/core/keybindings.js +2 -0
  13. package/dist/core/keybindings.js.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/assistant-message.js +7 -2
  19. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  20. package/dist/modes/interactive/components/footer.d.ts +10 -25
  21. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/footer.js +27 -145
  23. package/dist/modes/interactive/components/footer.js.map +1 -1
  24. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  25. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/model-selector.js +10 -2
  27. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  28. package/dist/modes/interactive/interactive-mode.d.ts +6 -0
  29. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/dist/modes/interactive/interactive-mode.js +110 -20
  31. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/docs/tui.md +10 -9
  33. package/examples/extensions/custom-footer.ts +33 -55
  34. package/examples/extensions/with-deps/package-lock.json +2 -2
  35. package/examples/extensions/with-deps/package.json +1 -1
  36. package/package.json +4 -4
@@ -10,6 +10,7 @@ import { getOAuthProviders, } from "@mariozechner/pi-ai";
10
10
  import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
11
11
  import { spawn, spawnSync } from "child_process";
12
12
  import { APP_NAME, getAuthPath, getDebugLogPath, isBunBinary, VERSION } from "../../config.js";
13
+ import { FooterDataProvider } from "../../core/footer-data-provider.js";
13
14
  import { KeybindingsManager } from "../../core/keybindings.js";
14
15
  import { createCompactionSummaryMessage } from "../../core/messages.js";
15
16
  import { SessionManager } from "../../core/session-manager.js";
@@ -57,6 +58,7 @@ export class InteractiveMode {
57
58
  autocompleteProvider;
58
59
  editorContainer;
59
60
  footer;
61
+ footerDataProvider;
60
62
  keybindings;
61
63
  version;
62
64
  isInitialized = false;
@@ -132,7 +134,8 @@ export class InteractiveMode {
132
134
  this.editor = this.defaultEditor;
133
135
  this.editorContainer = new Container();
134
136
  this.editorContainer.addChild(this.editor);
135
- this.footer = new FooterComponent(session);
137
+ this.footerDataProvider = new FooterDataProvider();
138
+ this.footer = new FooterComponent(session, this.footerDataProvider);
136
139
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
137
140
  // Load hide thinking block setting
138
141
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
@@ -201,6 +204,7 @@ export class InteractiveMode {
201
204
  const toggleThinking = formatStartupKey(kb.getKeys("toggleThinking"));
202
205
  const externalEditor = formatStartupKey(kb.getKeys("externalEditor"));
203
206
  const followUp = formatStartupKey(kb.getKeys("followUp"));
207
+ const dequeue = formatStartupKey(kb.getKeys("dequeue"));
204
208
  const instructions = theme.fg("dim", interrupt) +
205
209
  theme.fg("muted", " to interrupt") +
206
210
  "\n" +
@@ -249,6 +253,9 @@ export class InteractiveMode {
249
253
  theme.fg("dim", followUp) +
250
254
  theme.fg("muted", " to queue follow-up") +
251
255
  "\n" +
256
+ theme.fg("dim", dequeue) +
257
+ theme.fg("muted", " to restore queued messages") +
258
+ "\n" +
252
259
  theme.fg("dim", "ctrl+v") +
253
260
  theme.fg("muted", " to paste image") +
254
261
  "\n" +
@@ -280,7 +287,6 @@ export class InteractiveMode {
280
287
  this.ui.addChild(this.pendingMessagesContainer);
281
288
  this.ui.addChild(this.statusContainer);
282
289
  this.ui.addChild(this.widgetContainer);
283
- this.ui.addChild(new Spacer(1));
284
290
  this.ui.addChild(this.editorContainer);
285
291
  this.ui.addChild(this.footer);
286
292
  this.ui.setFocus(this.editor);
@@ -302,8 +308,8 @@ export class InteractiveMode {
302
308
  this.updateEditorBorderColor();
303
309
  this.ui.requestRender();
304
310
  });
305
- // Set up git branch watcher
306
- this.footer.watchBranch(() => {
311
+ // Set up git branch watcher (uses provider instead of footer)
312
+ this.footerDataProvider.onBranchChange(() => {
307
313
  this.ui.requestRender();
308
314
  });
309
315
  }
@@ -621,7 +627,7 @@ export class InteractiveMode {
621
627
  * Set extension status text in the footer.
622
628
  */
623
629
  setExtensionStatus(key, text) {
624
- this.footer.setExtensionStatus(key, text);
630
+ this.footerDataProvider.setExtensionStatus(key, text);
625
631
  this.ui.requestRender();
626
632
  }
627
633
  /**
@@ -663,9 +669,11 @@ export class InteractiveMode {
663
669
  return;
664
670
  this.widgetContainer.clear();
665
671
  if (this.extensionWidgets.size === 0) {
672
+ this.widgetContainer.addChild(new Spacer(1));
666
673
  this.ui.requestRender();
667
674
  return;
668
675
  }
676
+ this.widgetContainer.addChild(new Spacer(1));
669
677
  for (const [_key, component] of this.extensionWidgets) {
670
678
  this.widgetContainer.addChild(component);
671
679
  }
@@ -687,8 +695,8 @@ export class InteractiveMode {
687
695
  this.ui.removeChild(this.footer);
688
696
  }
689
697
  if (factory) {
690
- // Create and add custom footer
691
- this.customFooter = factory(this.ui, theme);
698
+ // Create and add custom footer, passing the data provider
699
+ this.customFooter = factory(this.ui, theme, this.footerDataProvider);
692
700
  this.ui.addChild(this.customFooter);
693
701
  }
694
702
  else {
@@ -1028,15 +1036,7 @@ export class InteractiveMode {
1028
1036
  // so they work correctly regardless of which editor is active
1029
1037
  this.defaultEditor.onEscape = () => {
1030
1038
  if (this.loadingAnimation) {
1031
- // Abort and restore queued messages to editor
1032
- const { steering, followUp } = this.session.clearQueue();
1033
- const allQueued = [...steering, ...followUp];
1034
- const queuedText = allQueued.join("\n\n");
1035
- const currentText = this.editor.getText();
1036
- const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
1037
- this.editor.setText(combinedText);
1038
- this.updatePendingMessagesDisplay();
1039
- this.agent.abort();
1039
+ this.restoreQueuedMessagesToEditor({ abort: true });
1040
1040
  }
1041
1041
  else if (this.session.isBashRunning) {
1042
1042
  this.session.abortBash();
@@ -1077,6 +1077,7 @@ export class InteractiveMode {
1077
1077
  this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
1078
1078
  this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor());
1079
1079
  this.defaultEditor.onAction("followUp", () => this.handleFollowUp());
1080
+ this.defaultEditor.onAction("dequeue", () => this.handleDequeue());
1080
1081
  this.defaultEditor.onChange = (text) => {
1081
1082
  const wasBashMode = this.isBashMode;
1082
1083
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1120,9 +1121,10 @@ export class InteractiveMode {
1120
1121
  this.editor.setText("");
1121
1122
  return;
1122
1123
  }
1123
- if (text === "/model") {
1124
- this.showModelSelector();
1124
+ if (text === "/model" || text.startsWith("/model ")) {
1125
+ const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
1125
1126
  this.editor.setText("");
1127
+ await this.handleModelCommand(searchTerm);
1126
1128
  return;
1127
1129
  }
1128
1130
  if (text.startsWith("/export")) {
@@ -1754,6 +1756,15 @@ export class InteractiveMode {
1754
1756
  this.editor.onSubmit(text);
1755
1757
  }
1756
1758
  }
1759
+ handleDequeue() {
1760
+ const restored = this.restoreQueuedMessagesToEditor();
1761
+ if (restored === 0) {
1762
+ this.showStatus("No queued messages to restore");
1763
+ }
1764
+ else {
1765
+ this.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`);
1766
+ }
1767
+ }
1757
1768
  updateEditorBorderColor() {
1758
1769
  if (this.isBashMode) {
1759
1770
  this.editor.borderColor = theme.getBashModeBorderColor();
@@ -1907,6 +1918,26 @@ export class InteractiveMode {
1907
1918
  }
1908
1919
  }
1909
1920
  }
1921
+ restoreQueuedMessagesToEditor(options) {
1922
+ const { steering, followUp } = this.session.clearQueue();
1923
+ const allQueued = [...steering, ...followUp];
1924
+ if (allQueued.length === 0) {
1925
+ this.updatePendingMessagesDisplay();
1926
+ if (options?.abort) {
1927
+ this.agent.abort();
1928
+ }
1929
+ return 0;
1930
+ }
1931
+ const queuedText = allQueued.join("\n\n");
1932
+ const currentText = options?.currentText ?? this.editor.getText();
1933
+ const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
1934
+ this.editor.setText(combinedText);
1935
+ this.updatePendingMessagesDisplay();
1936
+ if (options?.abort) {
1937
+ this.agent.abort();
1938
+ }
1939
+ return allQueued.length;
1940
+ }
1910
1941
  queueCompactionMessage(text, mode) {
1911
1942
  this.compactionQueuedMessages.push({ text, mode });
1912
1943
  this.editor.addToHistory?.(text);
@@ -2106,7 +2137,63 @@ export class InteractiveMode {
2106
2137
  return { component: selector, focus: selector.getSettingsList() };
2107
2138
  });
2108
2139
  }
2109
- showModelSelector() {
2140
+ async handleModelCommand(searchTerm) {
2141
+ if (!searchTerm) {
2142
+ this.showModelSelector();
2143
+ return;
2144
+ }
2145
+ const model = await this.findExactModelMatch(searchTerm);
2146
+ if (model) {
2147
+ try {
2148
+ await this.session.setModel(model);
2149
+ this.footer.invalidate();
2150
+ this.updateEditorBorderColor();
2151
+ this.showStatus(`Model: ${model.id}`);
2152
+ }
2153
+ catch (error) {
2154
+ this.showError(error instanceof Error ? error.message : String(error));
2155
+ }
2156
+ return;
2157
+ }
2158
+ this.showModelSelector(searchTerm);
2159
+ }
2160
+ async findExactModelMatch(searchTerm) {
2161
+ const term = searchTerm.trim();
2162
+ if (!term)
2163
+ return undefined;
2164
+ let targetProvider;
2165
+ let targetModelId = "";
2166
+ if (term.includes("/")) {
2167
+ const parts = term.split("/", 2);
2168
+ targetProvider = parts[0]?.trim().toLowerCase();
2169
+ targetModelId = parts[1]?.trim().toLowerCase() ?? "";
2170
+ }
2171
+ else {
2172
+ targetModelId = term.toLowerCase();
2173
+ }
2174
+ if (!targetModelId)
2175
+ return undefined;
2176
+ const models = await this.getModelCandidates();
2177
+ const exactMatches = models.filter((item) => {
2178
+ const idMatch = item.id.toLowerCase() === targetModelId;
2179
+ const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;
2180
+ return idMatch && providerMatch;
2181
+ });
2182
+ return exactMatches.length === 1 ? exactMatches[0] : undefined;
2183
+ }
2184
+ async getModelCandidates() {
2185
+ if (this.session.scopedModels.length > 0) {
2186
+ return this.session.scopedModels.map((scoped) => scoped.model);
2187
+ }
2188
+ this.session.modelRegistry.refresh();
2189
+ try {
2190
+ return await this.session.modelRegistry.getAvailable();
2191
+ }
2192
+ catch {
2193
+ return [];
2194
+ }
2195
+ }
2196
+ showModelSelector(initialSearchInput) {
2110
2197
  this.showSelector((done) => {
2111
2198
  const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
2112
2199
  try {
@@ -2123,7 +2210,7 @@ export class InteractiveMode {
2123
2210
  }, () => {
2124
2211
  done();
2125
2212
  this.ui.requestRender();
2126
- });
2213
+ }, initialSearchInput);
2127
2214
  return { component: selector, focus: selector };
2128
2215
  });
2129
2216
  }
@@ -2592,6 +2679,7 @@ export class InteractiveMode {
2592
2679
  const toggleThinking = this.getAppKeyDisplay("toggleThinking");
2593
2680
  const externalEditor = this.getAppKeyDisplay("externalEditor");
2594
2681
  const followUp = this.getAppKeyDisplay("followUp");
2682
+ const dequeue = this.getAppKeyDisplay("dequeue");
2595
2683
  let hotkeys = `
2596
2684
  **Navigation**
2597
2685
  | Key | Action |
@@ -2624,6 +2712,7 @@ export class InteractiveMode {
2624
2712
  | \`${toggleThinking}\` | Toggle thinking block visibility |
2625
2713
  | \`${externalEditor}\` | Edit message in external editor |
2626
2714
  | \`${followUp}\` | Queue follow-up message |
2715
+ | \`${dequeue}\` | Restore queued messages |
2627
2716
  | \`Ctrl+V\` | Paste image from clipboard |
2628
2717
  | \`/\` | Slash commands |
2629
2718
  | \`!\` | Run bash command |
@@ -2829,6 +2918,7 @@ export class InteractiveMode {
2829
2918
  this.loadingAnimation = undefined;
2830
2919
  }
2831
2920
  this.footer.dispose();
2921
+ this.footerDataProvider.dispose();
2832
2922
  if (this.unsubscribe) {
2833
2923
  this.unsubscribe();
2834
2924
  }