@earendil-works/pi-coding-agent 0.81.1 → 0.82.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 (115) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +13 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config-selector.d.ts.map +1 -1
  7. package/dist/cli/config-selector.js +1 -1
  8. package/dist/cli/config-selector.js.map +1 -1
  9. package/dist/cli/startup-ui.d.ts.map +1 -1
  10. package/dist/cli/startup-ui.js +1 -1
  11. package/dist/cli/startup-ui.js.map +1 -1
  12. package/dist/core/agent-session.d.ts +11 -0
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +7 -8
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/compaction/compaction.d.ts.map +1 -1
  17. package/dist/core/compaction/compaction.js +11 -3
  18. package/dist/core/compaction/compaction.js.map +1 -1
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +4 -0
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +7 -1
  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/model-config.d.ts +30 -0
  26. package/dist/core/model-config.d.ts.map +1 -1
  27. package/dist/core/model-config.js +6 -0
  28. package/dist/core/model-config.js.map +1 -1
  29. package/dist/core/model-registry.d.ts.map +1 -1
  30. package/dist/core/model-registry.js +2 -2
  31. package/dist/core/model-registry.js.map +1 -1
  32. package/dist/core/model-resolver.d.ts +1 -0
  33. package/dist/core/model-resolver.d.ts.map +1 -1
  34. package/dist/core/model-resolver.js +20 -3
  35. package/dist/core/model-resolver.js.map +1 -1
  36. package/dist/core/model-runtime.d.ts +0 -1
  37. package/dist/core/model-runtime.d.ts.map +1 -1
  38. package/dist/core/model-runtime.js +3 -5
  39. package/dist/core/model-runtime.js.map +1 -1
  40. package/dist/core/remote-catalog-provider.d.ts +1 -1
  41. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  42. package/dist/core/remote-catalog-provider.js +24 -11
  43. package/dist/core/remote-catalog-provider.js.map +1 -1
  44. package/dist/core/resource-loader.d.ts.map +1 -1
  45. package/dist/core/resource-loader.js +3 -0
  46. package/dist/core/resource-loader.js.map +1 -1
  47. package/dist/core/settings-manager.d.ts +1 -1
  48. package/dist/core/settings-manager.d.ts.map +1 -1
  49. package/dist/core/settings-manager.js.map +1 -1
  50. package/dist/core/system-prompt.d.ts.map +1 -1
  51. package/dist/core/system-prompt.js +1 -1
  52. package/dist/core/system-prompt.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +2 -0
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +34 -5
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  58. package/dist/core/tools/tool-definition-wrapper.js +3 -1
  59. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  60. package/dist/extensions/llama/provider.d.ts.map +1 -1
  61. package/dist/extensions/llama/provider.js +7 -2
  62. package/dist/extensions/llama/provider.js.map +1 -1
  63. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  64. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/custom-message.js +10 -2
  66. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  67. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  68. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/extension-editor.js +16 -46
  70. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  71. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  72. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  73. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  74. package/dist/modes/interactive/external-editor.d.ts +12 -0
  75. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  76. package/dist/modes/interactive/external-editor.js +37 -0
  77. package/dist/modes/interactive/external-editor.js.map +1 -0
  78. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.js +52 -58
  81. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  82. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  83. package/dist/modes/rpc/rpc-mode.js +1 -0
  84. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  85. package/dist/utils/clipboard.d.ts.map +1 -1
  86. package/dist/utils/clipboard.js +19 -8
  87. package/dist/utils/clipboard.js.map +1 -1
  88. package/dist/utils/version-check.d.ts.map +1 -1
  89. package/dist/utils/version-check.js +3 -1
  90. package/dist/utils/version-check.js.map +1 -1
  91. package/docs/compaction.md +1 -1
  92. package/docs/custom-provider.md +5 -2
  93. package/docs/environment-variables.md +88 -0
  94. package/docs/extensions.md +13 -5
  95. package/docs/index.md +1 -0
  96. package/docs/models.md +6 -2
  97. package/docs/providers.md +8 -1
  98. package/docs/rpc.md +23 -6
  99. package/docs/settings.md +1 -1
  100. package/docs/usage.md +0 -13
  101. package/examples/extensions/custom-compaction.ts +3 -0
  102. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  103. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  104. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  105. package/examples/extensions/gondolin/package-lock.json +2 -2
  106. package/examples/extensions/gondolin/package.json +1 -1
  107. package/examples/extensions/handoff.ts +9 -1
  108. package/examples/extensions/message-renderer.ts +2 -2
  109. package/examples/extensions/sandbox/package-lock.json +2 -2
  110. package/examples/extensions/sandbox/package.json +1 -1
  111. package/examples/extensions/summarize.ts +3 -0
  112. package/examples/extensions/with-deps/package-lock.json +2 -2
  113. package/examples/extensions/with-deps/package.json +1 -1
  114. package/npm-shrinkwrap.json +16 -15
  115. package/package.json +5 -4
@@ -17,7 +17,7 @@ import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
17
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
18
18
  import { KeybindingsManager } from "../../core/keybindings.js";
19
19
  import { createCompactionSummaryMessage } from "../../core/messages.js";
20
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
20
+ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope, resolveModelScopeWithDiagnostics, } from "../../core/model-resolver.js";
21
21
  import { DefaultPackageManager } from "../../core/package-manager.js";
22
22
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
23
23
  import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
@@ -64,6 +64,7 @@ import { TreeSelectorComponent } from "./components/tree-selector.js";
64
64
  import { TrustSelectorComponent } from "./components/trust-selector.js";
65
65
  import { UserMessageComponent } from "./components/user-message.js";
66
66
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
67
+ import { editInExternalEditor } from "./external-editor.js";
67
68
  import { getModelSearchText } from "./model-search.js";
68
69
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
69
70
  import { InteractiveThemeController } from "./theme/theme-controller.js";
@@ -272,7 +273,7 @@ export class InteractiveMode {
272
273
  await this.rebindCurrentSession({ renderBeforeBind: true });
273
274
  });
274
275
  this.version = VERSION;
275
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
276
+ this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor(), getAgentDir());
276
277
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
277
278
  this.headerContainer = new Container();
278
279
  this.loadedResourcesContainer = new Container();
@@ -797,8 +798,15 @@ export class InteractiveMode {
797
798
  * Get a short path relative to the package root for display.
798
799
  */
799
800
  getShortPath(fullPath, sourceInfo) {
801
+ const normalizedFullPath = fullPath.replace(/\\/g, "/");
800
802
  const baseDir = sourceInfo?.baseDir;
801
803
  if (baseDir && this.isPackageSource(sourceInfo)) {
804
+ const normalizedBaseDir = baseDir.replace(/\\/g, "/");
805
+ const npmRootMatch = normalizedBaseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
806
+ // If fullPath is under the same node_modules root as baseDir, preserve that relative topology.
807
+ if (npmRootMatch?.[1] && normalizedFullPath.startsWith(`${npmRootMatch[1]}/`)) {
808
+ return path.posix.relative(normalizedBaseDir, normalizedFullPath);
809
+ }
802
810
  const relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));
803
811
  if (relativePath &&
804
812
  relativePath !== "." &&
@@ -809,11 +817,11 @@ export class InteractiveMode {
809
817
  }
810
818
  }
811
819
  const source = sourceInfo?.source ?? "";
812
- const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
820
+ const npmMatch = normalizedFullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
813
821
  if (npmMatch && source.startsWith("npm:")) {
814
822
  return npmMatch[2];
815
823
  }
816
- const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
824
+ const gitMatch = normalizedFullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
817
825
  if (gitMatch && source.startsWith("git:")) {
818
826
  return gitMatch[1];
819
827
  }
@@ -1353,6 +1361,7 @@ export class InteractiveMode {
1353
1361
  sessionManager: this.sessionManager,
1354
1362
  modelRegistry: extensionRunner.getModelRegistry(),
1355
1363
  model: this.session.model,
1364
+ thinkingLevel: this.session.thinkingLevel,
1356
1365
  isIdle: () => this.session.isIdle,
1357
1366
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1358
1367
  signal: this.session.agent.signal,
@@ -2038,7 +2047,7 @@ export class InteractiveMode {
2038
2047
  this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
2039
2048
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2040
2049
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2041
- this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2050
+ this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2042
2051
  this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
2043
2052
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2044
2053
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
@@ -2400,6 +2409,9 @@ export class InteractiveMode {
2400
2409
  }
2401
2410
  this.ui.requestRender();
2402
2411
  break;
2412
+ case "bash_execution_update":
2413
+ // The bash execution callback handles TUI output rendering.
2414
+ break;
2403
2415
  case "tool_execution_start": {
2404
2416
  let component = this.pendingTools.get(event.toolCallId);
2405
2417
  if (!component) {
@@ -2610,7 +2622,7 @@ export class InteractiveMode {
2610
2622
  case "custom": {
2611
2623
  if (message.display) {
2612
2624
  const renderer = this.session.extensionRunner.getMessageRenderer(message.customType);
2613
- const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
2625
+ const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings(), this.outputPad);
2614
2626
  component.setExpanded(this.toolOutputExpanded);
2615
2627
  this.chatContainer.addChild(component);
2616
2628
  }
@@ -3122,51 +3134,21 @@ export class InteractiveMode {
3122
3134
  }
3123
3135
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
3124
3136
  }
3125
- async openExternalEditor() {
3137
+ async handleOpenExternalEditor() {
3126
3138
  const editorCmd = this.settingsManager.getExternalEditorCommand();
3127
- if (!editorCmd) {
3128
- this.showWarning("No editor configured. Set externalEditor in settings.json or $VISUAL/$EDITOR.");
3129
- return;
3130
- }
3131
- const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
3132
- const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
3139
+ const content = this.editor.getExpandedText?.() ?? this.editor.getText();
3140
+ this.ui.stop();
3133
3141
  try {
3134
- // Write current content to temp file
3135
- fs.writeFileSync(tmpFile, currentText, "utf-8");
3136
- // Stop TUI to release terminal
3137
- this.ui.stop();
3138
- // Split by space to support editor arguments (e.g., "code --wait")
3139
- const [editor, ...editorArgs] = editorCmd.split(" ");
3140
- process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`);
3141
- // Do not use spawnSync here. On Windows, synchronous child_process calls can keep
3142
- // Node/libuv's console input read active after ui.stop() pauses stdin, racing
3143
- // vim/nvim for the console input buffer until Ctrl+C cancels the pending read.
3144
- const status = await new Promise((resolve) => {
3145
- const child = spawn(editor, [...editorArgs, tmpFile], {
3146
- stdio: "inherit",
3147
- shell: process.platform === "win32",
3148
- });
3149
- child.on("error", () => resolve(null));
3150
- child.on("close", (code) => resolve(code));
3142
+ const result = await editInExternalEditor({
3143
+ command: editorCmd,
3144
+ content,
3151
3145
  });
3152
- // On successful exit (status 0), replace editor content
3153
- if (status === 0) {
3154
- const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
3155
- this.editor.setText(newContent);
3146
+ if (result.status === "complete") {
3147
+ this.editor.setText(result.content);
3156
3148
  }
3157
- // On non-zero exit, keep original text (no action needed)
3158
3149
  }
3159
3150
  finally {
3160
- // Clean up temp file
3161
- try {
3162
- fs.unlinkSync(tmpFile);
3163
- }
3164
- catch {
3165
- // Ignore cleanup errors
3166
- }
3167
- // Restart TUI
3168
3151
  this.ui.start();
3169
- // Force full re-render since external editor uses alternate screen
3170
3152
  this.ui.requestRender(true);
3171
3153
  }
3172
3154
  }
@@ -3543,7 +3525,9 @@ export class InteractiveMode {
3543
3525
  this.outputPad = padding;
3544
3526
  if (this.streamingComponent || this.session.isStreaming) {
3545
3527
  for (const child of this.chatContainer.children) {
3546
- if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent) {
3528
+ if (child instanceof AssistantMessageComponent ||
3529
+ child instanceof CustomMessageComponent ||
3530
+ child instanceof UserMessageComponent) {
3547
3531
  child.setOutputPad(padding);
3548
3532
  }
3549
3533
  }
@@ -3728,12 +3712,17 @@ export class InteractiveMode {
3728
3712
  // Get all available models
3729
3713
  await this.session.modelRuntime.refresh();
3730
3714
  const allModels = [...(await this.session.modelRuntime.getAvailable())];
3731
- if (allModels.length === 0) {
3715
+ const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
3716
+ const configuredPatterns = this.settingsManager.getEnabledModels();
3717
+ const sessionScopedModels = this.session.scopedModels;
3718
+ if (allModels.length === 0 && !configuredPatterns?.length && sessionScopedModels.length === 0) {
3732
3719
  this.showStatus("No models available");
3733
3720
  return;
3734
3721
  }
3722
+ const configuredScope = configuredPatterns?.length
3723
+ ? await resolveModelScopeWithDiagnostics(configuredPatterns, this.session.modelRuntime)
3724
+ : undefined;
3735
3725
  // Check if session has scoped models (from previous session-only changes or CLI --models)
3736
- const sessionScopedModels = this.session.scopedModels;
3737
3726
  const hasSessionScope = sessionScopedModels.length > 0;
3738
3727
  // Build enabled model IDs from session state or settings
3739
3728
  let currentEnabledIds = null;
@@ -3741,18 +3730,22 @@ export class InteractiveMode {
3741
3730
  // Use current session's scoped models
3742
3731
  currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3743
3732
  }
3744
- else {
3745
- // Fall back to settings
3746
- const patterns = this.settingsManager.getEnabledModels();
3747
- if (patterns !== undefined && patterns.length > 0) {
3748
- const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
3749
- currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3750
- }
3733
+ else if (configuredScope) {
3734
+ currentEnabledIds = configuredScope.scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3735
+ }
3736
+ for (const diagnostic of configuredScope?.diagnostics ?? []) {
3737
+ if (diagnostic.code !== "no-match")
3738
+ continue;
3739
+ currentEnabledIds ??= [];
3740
+ if (!currentEnabledIds.includes(diagnostic.pattern))
3741
+ currentEnabledIds.push(diagnostic.pattern);
3751
3742
  }
3752
3743
  // Helper to update session's scoped models (session-only, no persist)
3753
3744
  const updateSessionModels = async (enabledIds) => {
3754
3745
  currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3755
- if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3746
+ const hasEnabledAvailableModel = enabledIds?.some((id) => allModelIds.has(id)) ?? false;
3747
+ const allAvailableModelsEnabled = enabledIds !== null && [...allModelIds].every((id) => enabledIds.includes(id));
3748
+ if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
3756
3749
  const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
3757
3750
  this.session.setScopedModels(newScopedModels.map((sm) => ({
3758
3751
  model: sm.model,
@@ -3776,9 +3769,10 @@ export class InteractiveMode {
3776
3769
  },
3777
3770
  onPersist: (enabledIds) => {
3778
3771
  // Persist to settings
3779
- const newPatterns = enabledIds === null || enabledIds.length === allModels.length
3780
- ? undefined // All enabled = clear filter
3781
- : enabledIds;
3772
+ const allEnabled = enabledIds !== null &&
3773
+ enabledIds.length === allModels.length &&
3774
+ enabledIds.every((id) => allModelIds.has(id));
3775
+ const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
3782
3776
  this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3783
3777
  this.showStatus("Model selection saved to settings");
3784
3778
  },