@mariozechner/pi-coding-agent 0.38.0 → 0.39.1

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 (109) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -0
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +5 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +23 -0
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +62 -34
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts +6 -0
  12. package/dist/core/bash-executor.d.ts.map +1 -1
  13. package/dist/core/bash-executor.js +77 -0
  14. package/dist/core/bash-executor.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +1 -1
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js.map +1 -1
  18. package/dist/core/extensions/runner.d.ts +4 -3
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +44 -7
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +39 -3
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/index.d.ts +1 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/sdk.d.ts +5 -2
  30. package/dist/core/sdk.d.ts.map +1 -1
  31. package/dist/core/sdk.js +20 -12
  32. package/dist/core/sdk.js.map +1 -1
  33. package/dist/core/system-prompt.d.ts.map +1 -1
  34. package/dist/core/system-prompt.js +1 -5
  35. package/dist/core/system-prompt.js.map +1 -1
  36. package/dist/core/tools/bash.d.ts +25 -1
  37. package/dist/core/tools/bash.d.ts.map +1 -1
  38. package/dist/core/tools/bash.js +103 -73
  39. package/dist/core/tools/bash.js.map +1 -1
  40. package/dist/core/tools/edit.d.ts +17 -1
  41. package/dist/core/tools/edit.d.ts.map +1 -1
  42. package/dist/core/tools/edit.js +12 -5
  43. package/dist/core/tools/edit.js.map +1 -1
  44. package/dist/core/tools/find.d.ts +18 -1
  45. package/dist/core/tools/find.d.ts.map +1 -1
  46. package/dist/core/tools/find.js +68 -18
  47. package/dist/core/tools/find.js.map +1 -1
  48. package/dist/core/tools/grep.d.ts +15 -1
  49. package/dist/core/tools/grep.d.ts.map +1 -1
  50. package/dist/core/tools/grep.js +22 -10
  51. package/dist/core/tools/grep.js.map +1 -1
  52. package/dist/core/tools/index.d.ts +7 -7
  53. package/dist/core/tools/index.d.ts.map +1 -1
  54. package/dist/core/tools/index.js +1 -1
  55. package/dist/core/tools/index.js.map +1 -1
  56. package/dist/core/tools/ls.d.ts +21 -1
  57. package/dist/core/tools/ls.d.ts.map +1 -1
  58. package/dist/core/tools/ls.js +80 -72
  59. package/dist/core/tools/ls.js.map +1 -1
  60. package/dist/core/tools/read.d.ts +14 -0
  61. package/dist/core/tools/read.d.ts.map +1 -1
  62. package/dist/core/tools/read.js +12 -5
  63. package/dist/core/tools/read.js.map +1 -1
  64. package/dist/core/tools/write.d.ts +15 -1
  65. package/dist/core/tools/write.d.ts.map +1 -1
  66. package/dist/core/tools/write.js +9 -4
  67. package/dist/core/tools/write.js.map +1 -1
  68. package/dist/index.d.ts +3 -3
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +11 -1
  74. package/dist/main.js.map +1 -1
  75. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/assistant-message.js +7 -3
  77. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  78. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  79. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  80. package/dist/modes/interactive/components/tool-execution.js +50 -23
  81. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  82. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  83. package/dist/modes/interactive/interactive-mode.js +153 -46
  84. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  85. package/dist/modes/interactive/theme/theme.d.ts +7 -0
  86. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  87. package/dist/modes/interactive/theme/theme.js +40 -0
  88. package/dist/modes/interactive/theme/theme.js.map +1 -1
  89. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  90. package/dist/modes/rpc/rpc-mode.js +10 -0
  91. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  92. package/dist/utils/clipboard.d.ts.map +1 -1
  93. package/dist/utils/clipboard.js +35 -7
  94. package/dist/utils/clipboard.js.map +1 -1
  95. package/docs/extensions.md +101 -6
  96. package/docs/sdk.md +3 -3
  97. package/examples/extensions/README.md +2 -0
  98. package/examples/extensions/claude-rules.ts +5 -2
  99. package/examples/extensions/interactive-shell.ts +196 -0
  100. package/examples/extensions/mac-system-theme.ts +45 -0
  101. package/examples/extensions/overlay-test.ts +145 -0
  102. package/examples/extensions/pirate.ts +7 -4
  103. package/examples/extensions/preset.ts +2 -2
  104. package/examples/extensions/ssh.ts +220 -0
  105. package/examples/extensions/tool-override.ts +143 -0
  106. package/examples/extensions/with-deps/package-lock.json +2 -2
  107. package/examples/extensions/with-deps/package.json +1 -1
  108. package/examples/sdk/04-skills.ts +4 -1
  109. package/package.json +5 -5
@@ -9,11 +9,10 @@ import * as path from "node:path";
9
9
  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
- import { APP_NAME, getAuthPath, getDebugLogPath, VERSION } from "../../config.js";
12
+ import { APP_NAME, getAuthPath, getDebugLogPath, isBunBinary, VERSION } from "../../config.js";
13
13
  import { KeybindingsManager } from "../../core/keybindings.js";
14
14
  import { createCompactionSummaryMessage } from "../../core/messages.js";
15
15
  import { SessionManager } from "../../core/session-manager.js";
16
- import { loadSkills } from "../../core/skills.js";
17
16
  import { loadProjectContextFiles } from "../../core/system-prompt.js";
18
17
  import { allTools } from "../../core/tools/index.js";
19
18
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
@@ -42,7 +41,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
42
41
  import { TreeSelectorComponent } from "./components/tree-selector.js";
43
42
  import { UserMessageComponent } from "./components/user-message.js";
44
43
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
45
- import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme, } from "./theme/theme.js";
44
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setTheme, setThemeInstance, Theme, theme, } from "./theme/theme.js";
46
45
  function isExpandable(obj) {
47
46
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
48
47
  }
@@ -137,6 +136,8 @@ export class InteractiveMode {
137
136
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
138
137
  // Load hide thinking block setting
139
138
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
139
+ // Initialize theme with watcher for interactive mode
140
+ initTheme(this.settingsManager.getTheme(), true);
140
141
  }
141
142
  setupAutocomplete(fdPath) {
142
143
  // Define commands for autocomplete
@@ -426,23 +427,19 @@ export class InteractiveMode {
426
427
  this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded context:\n") + contextList, 0, 0));
427
428
  this.chatContainer.addChild(new Spacer(1));
428
429
  }
429
- // Show loaded skills
430
- const skillsSettings = this.session.skillsSettings;
431
- if (skillsSettings?.enabled !== false) {
432
- const { skills, warnings: skillWarnings } = loadSkills(skillsSettings ?? {});
433
- if (skills.length > 0) {
434
- const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n");
435
- this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0));
436
- this.chatContainer.addChild(new Spacer(1));
437
- }
438
- // Show skill warnings if any
439
- if (skillWarnings.length > 0) {
440
- const warningList = skillWarnings
441
- .map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`))
442
- .join("\n");
443
- this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0));
444
- this.chatContainer.addChild(new Spacer(1));
445
- }
430
+ // Show loaded skills (already discovered by SDK)
431
+ const skills = this.session.skills;
432
+ if (skills.length > 0) {
433
+ const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n");
434
+ this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0));
435
+ this.chatContainer.addChild(new Spacer(1));
436
+ }
437
+ // Show skill warnings if any
438
+ const skillWarnings = this.session.skillWarnings;
439
+ if (skillWarnings.length > 0) {
440
+ const warningList = skillWarnings.map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)).join("\n");
441
+ this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0));
442
+ this.chatContainer.addChild(new Spacer(1));
446
443
  }
447
444
  const extensionRunner = this.session.extensionRunner;
448
445
  if (!extensionRunner) {
@@ -746,7 +743,7 @@ export class InteractiveMode {
746
743
  setFooter: (factory) => this.setExtensionFooter(factory),
747
744
  setHeader: (factory) => this.setExtensionHeader(factory),
748
745
  setTitle: (title) => this.ui.terminal.setTitle(title),
749
- custom: (factory) => this.showExtensionCustom(factory),
746
+ custom: (factory, options) => this.showExtensionCustom(factory, options),
750
747
  setEditorText: (text) => this.editor.setText(text),
751
748
  getEditorText: () => this.editor.getText(),
752
749
  editor: (title, prefill) => this.showExtensionEditor(title, prefill),
@@ -754,6 +751,20 @@ export class InteractiveMode {
754
751
  get theme() {
755
752
  return theme;
756
753
  },
754
+ getAllThemes: () => getAvailableThemesWithPaths(),
755
+ getTheme: (name) => getThemeByName(name),
756
+ setTheme: (themeOrName) => {
757
+ if (themeOrName instanceof Theme) {
758
+ setThemeInstance(themeOrName);
759
+ this.ui.requestRender();
760
+ return { success: true };
761
+ }
762
+ const result = setTheme(themeOrName, true);
763
+ if (result.success) {
764
+ this.ui.requestRender();
765
+ }
766
+ return result;
767
+ },
757
768
  };
758
769
  }
759
770
  /**
@@ -933,28 +944,59 @@ export class InteractiveMode {
933
944
  this.showStatus(message);
934
945
  }
935
946
  }
936
- /**
937
- * Show a custom component with keyboard focus.
938
- */
939
- async showExtensionCustom(factory) {
947
+ /** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
948
+ async showExtensionCustom(factory, options) {
940
949
  const savedText = this.editor.getText();
941
- return new Promise((resolve) => {
950
+ const isOverlay = options?.overlay ?? false;
951
+ const restoreEditor = () => {
952
+ this.editorContainer.clear();
953
+ this.editorContainer.addChild(this.editor);
954
+ this.editor.setText(savedText);
955
+ this.ui.setFocus(this.editor);
956
+ this.ui.requestRender();
957
+ };
958
+ return new Promise((resolve, reject) => {
942
959
  let component;
960
+ let closed = false;
943
961
  const close = (result) => {
944
- component.dispose?.();
945
- this.editorContainer.clear();
946
- this.editorContainer.addChild(this.editor);
947
- this.editor.setText(savedText);
948
- this.ui.setFocus(this.editor);
949
- this.ui.requestRender();
962
+ if (closed)
963
+ return;
964
+ closed = true;
965
+ if (isOverlay)
966
+ this.ui.hideOverlay();
967
+ else
968
+ restoreEditor();
969
+ // Note: both branches above already call requestRender
950
970
  resolve(result);
971
+ try {
972
+ component?.dispose?.();
973
+ }
974
+ catch {
975
+ /* ignore dispose errors */
976
+ }
951
977
  };
952
- Promise.resolve(factory(this.ui, theme, this.keybindings, close)).then((c) => {
978
+ Promise.resolve(factory(this.ui, theme, this.keybindings, close))
979
+ .then((c) => {
980
+ if (closed)
981
+ return;
953
982
  component = c;
954
- this.editorContainer.clear();
955
- this.editorContainer.addChild(component);
956
- this.ui.setFocus(component);
957
- this.ui.requestRender();
983
+ if (isOverlay) {
984
+ const w = component.width;
985
+ this.ui.showOverlay(component, w ? { width: w } : undefined);
986
+ }
987
+ else {
988
+ this.editorContainer.clear();
989
+ this.editorContainer.addChild(component);
990
+ this.ui.setFocus(component);
991
+ this.ui.requestRender();
992
+ }
993
+ })
994
+ .catch((err) => {
995
+ if (closed)
996
+ return;
997
+ if (!isOverlay)
998
+ restoreEditor();
999
+ reject(err);
958
1000
  });
959
1001
  });
960
1002
  }
@@ -1224,6 +1266,16 @@ export class InteractiveMode {
1224
1266
  this.footer.invalidate();
1225
1267
  switch (event.type) {
1226
1268
  case "agent_start":
1269
+ // Restore main escape handler if retry handler is still active
1270
+ // (retry success event fires later, but we need main handler now)
1271
+ if (this.retryEscapeHandler) {
1272
+ this.defaultEditor.onEscape = this.retryEscapeHandler;
1273
+ this.retryEscapeHandler = undefined;
1274
+ }
1275
+ if (this.retryLoader) {
1276
+ this.retryLoader.stop();
1277
+ this.retryLoader = undefined;
1278
+ }
1227
1279
  if (this.loadingAnimation) {
1228
1280
  this.loadingAnimation.stop();
1229
1281
  }
@@ -1281,11 +1333,20 @@ export class InteractiveMode {
1281
1333
  break;
1282
1334
  if (this.streamingComponent && event.message.role === "assistant") {
1283
1335
  this.streamingMessage = event.message;
1336
+ let errorMessage;
1337
+ if (this.streamingMessage.stopReason === "aborted") {
1338
+ const retryAttempt = this.session.retryAttempt;
1339
+ errorMessage =
1340
+ retryAttempt > 0
1341
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
1342
+ : "Operation aborted";
1343
+ this.streamingMessage.errorMessage = errorMessage;
1344
+ }
1284
1345
  this.streamingComponent.updateContent(this.streamingMessage);
1285
1346
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
1286
- const errorMessage = this.streamingMessage.stopReason === "aborted"
1287
- ? "Operation aborted"
1288
- : this.streamingMessage.errorMessage || "Error";
1347
+ if (!errorMessage) {
1348
+ errorMessage = this.streamingMessage.errorMessage || "Error";
1349
+ }
1289
1350
  for (const [, component] of this.pendingTools.entries()) {
1290
1351
  component.updateResult({
1291
1352
  content: [{ type: "text", text: errorMessage }],
@@ -1545,7 +1606,17 @@ export class InteractiveMode {
1545
1606
  component.setExpanded(this.toolOutputExpanded);
1546
1607
  this.chatContainer.addChild(component);
1547
1608
  if (message.stopReason === "aborted" || message.stopReason === "error") {
1548
- const errorMessage = message.stopReason === "aborted" ? "Operation aborted" : message.errorMessage || "Error";
1609
+ let errorMessage;
1610
+ if (message.stopReason === "aborted") {
1611
+ const retryAttempt = this.session.retryAttempt;
1612
+ errorMessage =
1613
+ retryAttempt > 0
1614
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
1615
+ : "Operation aborted";
1616
+ }
1617
+ else {
1618
+ errorMessage = message.errorMessage || "Error";
1619
+ }
1549
1620
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
1550
1621
  }
1551
1622
  else {
@@ -1803,12 +1874,14 @@ export class InteractiveMode {
1803
1874
  this.ui.requestRender();
1804
1875
  }
1805
1876
  showNewVersionNotification(newVersion) {
1877
+ const updateInstruction = isBunBinary
1878
+ ? theme.fg("muted", `New version ${newVersion} is available. Download from: `) +
1879
+ theme.fg("accent", "https://github.com/badlogic/pi-mono/releases/latest")
1880
+ : theme.fg("muted", `New version ${newVersion} is available. Run: `) +
1881
+ theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent");
1806
1882
  this.chatContainer.addChild(new Spacer(1));
1807
1883
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
1808
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("warning", "Update Available")) +
1809
- "\n" +
1810
- theme.fg("muted", `New version ${newVersion} is available. Run: `) +
1811
- theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"), 1, 0));
1884
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0));
1812
1885
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
1813
1886
  this.ui.requestRender();
1814
1887
  }
@@ -2632,6 +2705,40 @@ export class InteractiveMode {
2632
2705
  this.ui.requestRender();
2633
2706
  }
2634
2707
  async handleBashCommand(command, excludeFromContext = false) {
2708
+ const extensionRunner = this.session.extensionRunner;
2709
+ // Emit user_bash event to let extensions intercept
2710
+ const eventResult = extensionRunner
2711
+ ? await extensionRunner.emitUserBash({
2712
+ type: "user_bash",
2713
+ command,
2714
+ excludeFromContext,
2715
+ cwd: process.cwd(),
2716
+ })
2717
+ : undefined;
2718
+ // If extension returned a full result, use it directly
2719
+ if (eventResult?.result) {
2720
+ const result = eventResult.result;
2721
+ // Create UI component for display
2722
+ this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
2723
+ if (this.session.isStreaming) {
2724
+ this.pendingMessagesContainer.addChild(this.bashComponent);
2725
+ this.pendingBashComponents.push(this.bashComponent);
2726
+ }
2727
+ else {
2728
+ this.chatContainer.addChild(this.bashComponent);
2729
+ }
2730
+ // Show output and complete
2731
+ if (result.output) {
2732
+ this.bashComponent.appendOutput(result.output);
2733
+ }
2734
+ this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
2735
+ // Record the result in session
2736
+ this.session.recordBashResult(command, result, { excludeFromContext });
2737
+ this.bashComponent = undefined;
2738
+ this.ui.requestRender();
2739
+ return;
2740
+ }
2741
+ // Normal execution path (possibly with custom operations)
2635
2742
  const isDeferred = this.session.isStreaming;
2636
2743
  this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
2637
2744
  if (isDeferred) {
@@ -2650,7 +2757,7 @@ export class InteractiveMode {
2650
2757
  this.bashComponent.appendOutput(chunk);
2651
2758
  this.ui.requestRender();
2652
2759
  }
2653
- }, { excludeFromContext });
2760
+ }, { excludeFromContext, operations: eventResult?.operations });
2654
2761
  if (this.bashComponent) {
2655
2762
  this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
2656
2763
  }