@oh-my-pi/pi-coding-agent 13.12.10 → 13.13.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 +26 -1
  2. package/README.md +3 -1
  3. package/package.json +7 -7
  4. package/src/capability/mcp.ts +2 -0
  5. package/src/config/keybindings.ts +0 -4
  6. package/src/config/mcp-schema.json +230 -0
  7. package/src/config/settings-schema.ts +30 -1
  8. package/src/discovery/builtin.ts +1 -0
  9. package/src/discovery/mcp-json.ts +3 -0
  10. package/src/extensibility/custom-tools/types.ts +4 -0
  11. package/src/extensibility/extensions/types.ts +4 -0
  12. package/src/internal-urls/docs-index.generated.ts +2 -1
  13. package/src/main.ts +6 -0
  14. package/src/mcp/client.ts +46 -16
  15. package/src/mcp/config-writer.ts +9 -2
  16. package/src/mcp/config.ts +1 -0
  17. package/src/mcp/discoverable-tool-metadata.ts +10 -0
  18. package/src/mcp/manager.ts +28 -0
  19. package/src/mcp/transports/http.ts +148 -28
  20. package/src/mcp/transports/stdio.ts +62 -12
  21. package/src/mcp/types.ts +22 -1
  22. package/src/modes/components/custom-editor.ts +126 -71
  23. package/src/modes/controllers/command-controller.ts +3 -12
  24. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  25. package/src/modes/controllers/input-controller.ts +36 -37
  26. package/src/modes/controllers/selector-controller.ts +11 -0
  27. package/src/modes/interactive-mode.ts +5 -12
  28. package/src/modes/utils/hotkeys-markdown.ts +24 -22
  29. package/src/patch/index.ts +25 -0
  30. package/src/sdk.ts +19 -2
  31. package/src/session/agent-session.ts +37 -5
  32. package/src/session/compaction/compaction.ts +1 -1
  33. package/src/system-prompt.ts +26 -14
  34. package/src/tools/auto-generated-guard.ts +310 -0
  35. package/src/tools/write.ts +33 -2
  36. package/src/utils/title-generator.ts +46 -3
@@ -32,6 +32,7 @@ import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
32
32
  import { replaceTabs } from "../../tools/render-utils";
33
33
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
34
34
  import { openPath } from "../../utils/open";
35
+ import { setSessionTerminalTitle } from "../../utils/title-generator";
35
36
 
36
37
  export class CommandController {
37
38
  constructor(private readonly ctx: InteractiveModeContext) {}
@@ -505,18 +506,7 @@ export class CommandController {
505
506
  }
506
507
 
507
508
  handleHotkeysCommand(): void {
508
- const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || "Ctrl+O";
509
- const planModeKey = this.ctx.keybindings.getDisplayString("togglePlanMode") || "Alt+Shift+P";
510
- const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H";
511
- const copyLineKey = this.ctx.keybindings.getDisplayString("copyLine") || "Alt+Shift+L";
512
- const copyPromptKey = this.ctx.keybindings.getDisplayString("copyPrompt") || "Alt+Shift+C";
513
- const hotkeys = buildHotkeysMarkdown({
514
- expandToolsKey,
515
- planModeKey,
516
- sttKey,
517
- copyLineKey,
518
- copyPromptKey,
519
- });
509
+ const hotkeys = buildHotkeysMarkdown({ keybindings: this.ctx.keybindings });
520
510
  this.ctx.chatContainer.addChild(new Spacer(1));
521
511
  this.ctx.chatContainer.addChild(new DynamicBorder());
522
512
  this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
@@ -585,6 +575,7 @@ export class CommandController {
585
575
  }
586
576
  }
587
577
  await this.ctx.session.newSession();
578
+ setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
588
579
 
589
580
  this.ctx.statusLine.invalidate();
590
581
  this.ctx.statusLine.setSessionStartTime(Date.now());
@@ -16,7 +16,7 @@ import { HookInputComponent } from "../../modes/components/hook-input";
16
16
  import { HookSelectorComponent } from "../../modes/components/hook-selector";
17
17
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
18
18
  import type { InteractiveModeContext } from "../../modes/types";
19
- import { setTerminalTitle } from "../../utils/title-generator";
19
+ import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
20
20
 
21
21
  export class ExtensionUiController {
22
22
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
@@ -154,6 +154,7 @@ export class ExtensionUiController {
154
154
  if (!success) {
155
155
  return { cancelled: true };
156
156
  }
157
+ setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
157
158
 
158
159
  // Call setup callback if provided
159
160
  if (options?.setup) {
@@ -230,6 +231,7 @@ export class ExtensionUiController {
230
231
  if (!result) {
231
232
  return { cancelled: true };
232
233
  }
234
+ setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
233
235
  this.ctx.chatContainer.clear();
234
236
  this.ctx.renderInitialMessages();
235
237
  await this.ctx.reloadTodos();
@@ -12,7 +12,7 @@ import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../sessio
12
12
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
13
13
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
14
14
  import { resizeImage } from "../../utils/image-resize";
15
- import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
15
+ import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
16
16
 
17
17
  interface Expandable {
18
18
  setExpanded(expanded: boolean): void;
@@ -26,6 +26,7 @@ export class InputController {
26
26
  constructor(private ctx: InteractiveModeContext) {}
27
27
 
28
28
  setupKeyHandlers(): void {
29
+ this.ctx.editor.setActionKeys("interrupt", this.ctx.keybindings.getKeys("interrupt"));
29
30
  this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
30
31
  Boolean(
31
32
  this.ctx.loadingAnimation ||
@@ -64,7 +65,7 @@ export class InputController {
64
65
  } else if (this.ctx.session.isStreaming) {
65
66
  void this.ctx.session.abort();
66
67
  } else if (!this.ctx.editor.getText().trim()) {
67
- // Double-escape with empty editor triggers /tree, /branch, or nothing based on setting
68
+ // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
68
69
  const action = settings.get("doubleEscapeAction");
69
70
  if (action !== "none") {
70
71
  const now = Date.now();
@@ -82,42 +83,44 @@ export class InputController {
82
83
  }
83
84
  };
84
85
 
85
- this.ctx.editor.onCtrlC = () => this.handleCtrlC();
86
- this.ctx.editor.onCtrlD = () => this.handleCtrlD();
87
- this.ctx.editor.onCtrlZ = () => this.handleCtrlZ();
88
- this.ctx.editor.onShiftTab = () => this.cycleThinkingLevel();
89
- this.ctx.editor.onCtrlP = () => this.cycleRoleModel();
90
- this.ctx.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true });
91
- this.ctx.editor.onAltP = () => this.ctx.showModelSelector({ temporaryOnly: true });
86
+ this.ctx.editor.setActionKeys("clear", this.ctx.keybindings.getKeys("clear"));
87
+ this.ctx.editor.onClear = () => this.handleCtrlC();
88
+ this.ctx.editor.setActionKeys("exit", this.ctx.keybindings.getKeys("exit"));
89
+ this.ctx.editor.onExit = () => this.handleCtrlD();
90
+ this.ctx.editor.setActionKeys("suspend", this.ctx.keybindings.getKeys("suspend"));
91
+ this.ctx.editor.onSuspend = () => this.handleCtrlZ();
92
+ this.ctx.editor.setActionKeys("cycleThinkingLevel", this.ctx.keybindings.getKeys("cycleThinkingLevel"));
93
+ this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
94
+ this.ctx.editor.setActionKeys("cycleModelForward", this.ctx.keybindings.getKeys("cycleModelForward"));
95
+ this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
96
+ this.ctx.editor.setActionKeys("cycleModelBackward", this.ctx.keybindings.getKeys("cycleModelBackward"));
97
+ this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true });
98
+ this.ctx.editor.onQuickSelectModel = () => this.ctx.showModelSelector({ temporaryOnly: true });
92
99
 
93
100
  // Global debug handler on TUI (works regardless of focus)
94
101
  this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
95
- this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector();
96
- this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch();
97
- this.ctx.editor.onCtrlT = () => this.ctx.toggleTodoExpansion();
98
- this.ctx.editor.onCtrlG = () => void this.openExternalEditor();
99
- this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand();
100
- this.ctx.editor.onCtrlV = () => this.handleImagePaste();
101
- const copyPromptKeys = this.ctx.keybindings.getKeys("copyPrompt");
102
- this.ctx.editor.onCopyPrompt = copyPromptKeys.includes("alt+shift+c") ? () => this.handleCopyPrompt() : undefined;
103
-
102
+ this.ctx.editor.setActionKeys("selectModel", this.ctx.keybindings.getKeys("selectModel"));
103
+ this.ctx.editor.onSelectModel = () => this.ctx.showModelSelector();
104
+ this.ctx.editor.setActionKeys("historySearch", this.ctx.keybindings.getKeys("historySearch"));
105
+ this.ctx.editor.onHistorySearch = () => this.ctx.showHistorySearch();
106
+ this.ctx.editor.setActionKeys("toggleThinking", this.ctx.keybindings.getKeys("toggleThinking"));
107
+ this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
108
+ this.ctx.editor.setActionKeys("externalEditor", this.ctx.keybindings.getKeys("externalEditor"));
109
+ this.ctx.editor.onExternalEditor = () => void this.openExternalEditor();
110
+ this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand();
111
+ this.ctx.editor.setActionKeys("pasteImage", this.ctx.keybindings.getKeys("pasteImage"));
112
+ this.ctx.editor.onPasteImage = () => this.handleImagePaste();
113
+ this.ctx.editor.setActionKeys("copyPrompt", this.ctx.keybindings.getKeys("copyPrompt"));
114
+ this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
115
+ this.ctx.editor.setActionKeys("expandTools", this.ctx.keybindings.getKeys("expandTools"));
116
+ this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
117
+ this.ctx.editor.setActionKeys("dequeue", this.ctx.keybindings.getKeys("dequeue"));
118
+ this.ctx.editor.onDequeue = () => this.handleDequeue();
119
+
120
+ this.ctx.editor.clearCustomKeyHandlers();
104
121
  // Wire up extension shortcuts
105
122
  this.registerExtensionShortcuts();
106
123
 
107
- const expandToolsKeys = this.ctx.keybindings.getKeys("expandTools");
108
- this.ctx.editor.onCtrlO = expandToolsKeys.includes("ctrl+o") ? () => this.toggleToolOutputExpansion() : undefined;
109
- for (const key of expandToolsKeys) {
110
- if (key === "ctrl+o") continue;
111
- this.ctx.editor.setCustomKeyHandler(key, () => this.toggleToolOutputExpansion());
112
- }
113
-
114
- const dequeueKeys = this.ctx.keybindings.getKeys("dequeue");
115
- this.ctx.editor.onAltUp = dequeueKeys.includes("alt+up") ? () => this.handleDequeue() : undefined;
116
- for (const key of dequeueKeys) {
117
- if (key === "alt+up") continue;
118
- this.ctx.editor.setCustomKeyHandler(key, () => this.handleDequeue());
119
- }
120
-
121
124
  const planModeKeys = this.ctx.keybindings.getKeys("togglePlanMode");
122
125
  for (const key of planModeKeys) {
123
126
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
@@ -144,10 +147,6 @@ export class InputController {
144
147
  for (const key of this.ctx.keybindings.getKeys("copyLine")) {
145
148
  this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
146
149
  }
147
- for (const key of copyPromptKeys) {
148
- if (key === "alt+shift+c") continue;
149
- this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyPrompt());
150
- }
151
150
 
152
151
  this.ctx.editor.onChange = (text: string) => {
153
152
  const wasBashMode = this.ctx.isBashMode;
@@ -326,7 +325,7 @@ export class InputController {
326
325
  .then(async title => {
327
326
  if (title) {
328
327
  await this.ctx.sessionManager.setSessionName(title);
329
- setTerminalTitle(`π: ${title}`);
328
+ setSessionTerminalTitle(title, this.ctx.sessionManager.getCwd());
330
329
  }
331
330
  })
332
331
  .catch(() => {});
@@ -26,6 +26,7 @@ import {
26
26
  setPreferredImageProvider,
27
27
  setPreferredSearchProvider,
28
28
  } from "../../tools";
29
+ import { setSessionTerminalTitle } from "../../utils/title-generator";
29
30
  import { AgentDashboard } from "../components/agent-dashboard";
30
31
  import { AssistantMessageComponent } from "../components/assistant-message";
31
32
  import { ExtensionDashboard } from "../components/extensions";
@@ -638,6 +639,14 @@ export class SelectorController {
638
639
  this.ctx.pendingTools.clear();
639
640
  }
640
641
 
642
+ #refreshSessionTerminalTitle(): void {
643
+ const sessionManager = this.ctx.sessionManager as {
644
+ getSessionName?: () => string | undefined;
645
+ getCwd: () => string;
646
+ };
647
+ setSessionTerminalTitle(sessionManager.getSessionName?.(), sessionManager.getCwd());
648
+ }
649
+
641
650
  async #detachActiveSessionBeforeDeletion(sessionPath: string): Promise<boolean> {
642
651
  const currentSessionFile = this.ctx.sessionManager.getSessionFile();
643
652
  if (currentSessionFile !== sessionPath) {
@@ -648,6 +657,7 @@ export class SelectorController {
648
657
  if (!detached) {
649
658
  return false;
650
659
  }
660
+ this.#refreshSessionTerminalTitle();
651
661
 
652
662
  this.#clearTransientSessionUi();
653
663
  this.ctx.statusLine.invalidate();
@@ -664,6 +674,7 @@ export class SelectorController {
664
674
 
665
675
  // Switch session via AgentSession (emits hook and tool session events)
666
676
  await this.ctx.session.switchSession(sessionPath);
677
+ this.#refreshSessionTerminalTitle();
667
678
 
668
679
  // Clear and re-render the chat
669
680
  this.ctx.chatContainer.clear();
@@ -24,7 +24,7 @@ import type { SessionContext, SessionManager } from "../session/session-manager"
24
24
  import { getRecentSessions } from "../session/session-manager";
25
25
  import { STTController, type SttState } from "../stt";
26
26
  import type { ExitPlanModeDetails } from "../tools";
27
- import { setTerminalTitle } from "../utils/title-generator";
27
+ import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
28
28
  import type { AssistantMessageComponent } from "./components/assistant-message";
29
29
  import type { BashExecutionComponent } from "./components/bash-execution";
30
30
  import { CustomEditor } from "./components/custom-editor";
@@ -323,12 +323,6 @@ export class InteractiveMode implements InteractiveModeContext {
323
323
  }
324
324
  }
325
325
 
326
- // Set terminal title if session already has one (resumed session)
327
- const existingTitle = this.sessionManager.getSessionName();
328
- if (existingTitle) {
329
- setTerminalTitle(`pi: ${existingTitle}`);
330
- }
331
-
332
326
  this.ui.addChild(this.chatContainer);
333
327
  this.ui.addChild(this.pendingMessagesContainer);
334
328
  this.ui.addChild(this.statusContainer);
@@ -347,13 +341,12 @@ export class InteractiveMode implements InteractiveModeContext {
347
341
 
348
342
  // Start the UI
349
343
  this.ui.start();
344
+ pushTerminalTitle();
345
+ setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
350
346
  this.#syncEditorMaxHeight();
351
347
  this.isInitialized = true;
352
348
  this.ui.requestRender(true);
353
349
 
354
- // Set initial terminal title (will be updated when session title is generated)
355
- this.ui.terminal.setTitle("π");
356
-
357
350
  // Initialize hooks with TUI-based UI context
358
351
  await this.initHooksAndCustomTools();
359
352
 
@@ -555,7 +548,7 @@ export class InteractiveMode implements InteractiveModeContext {
555
548
  });
556
549
  if (visibleTasks.length < activePhase.tasks.length) {
557
550
  const remaining = activePhase.tasks.length - visibleTasks.length;
558
- lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more (Ctrl+T to expand)`));
551
+ lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more`));
559
552
  }
560
553
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
561
554
  return;
@@ -883,7 +876,7 @@ export class InteractiveMode implements InteractiveModeContext {
883
876
  // Drain any in-flight Kitty key release events before stopping.
884
877
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
885
878
  await this.ui.terminal.drainInput(1000);
886
-
879
+ popTerminalTitle();
887
880
  this.stop();
888
881
 
889
882
  // Print resumption hint if this is a persisted session
@@ -1,13 +1,14 @@
1
+ import type { AppAction, KeybindingsManager } from "../../config/keybindings";
2
+
1
3
  export interface HotkeysMarkdownBindings {
2
- expandToolsKey: string;
3
- planModeKey: string;
4
- sttKey: string;
5
- copyLineKey: string;
6
- copyPromptKey: string;
4
+ keybindings: Pick<KeybindingsManager, "getDisplayString">;
5
+ }
6
+
7
+ function appKey(bindings: HotkeysMarkdownBindings, action: AppAction): string {
8
+ return bindings.keybindings.getDisplayString(action) || "Disabled";
7
9
  }
8
10
 
9
11
  export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
10
- const { expandToolsKey, planModeKey, sttKey, copyLineKey, copyPromptKey } = bindings;
11
12
  return [
12
13
  "**Navigation**",
13
14
  "| Key | Action |",
@@ -25,28 +26,29 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
25
26
  "| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
26
27
  "| `Ctrl+U` | Delete to start of line |",
27
28
  "| `Ctrl+K` | Delete to end of line |",
28
- `| \`${copyLineKey}\` | Copy current line |`,
29
- `| \`${copyPromptKey}\` | Copy whole prompt |`,
29
+ `| \`${appKey(bindings, "copyLine")}\` | Copy current line |`,
30
+ `| \`${appKey(bindings, "copyPrompt")}\` | Copy whole prompt |`,
30
31
  "",
31
32
  "**Other**",
32
33
  "| Key | Action |",
33
34
  "|-----|--------|",
34
35
  "| `Tab` | Path completion / accept autocomplete |",
35
- "| `Escape` | Cancel autocomplete / abort streaming |",
36
- "| `Ctrl+C` | Clear editor (first) / exit (second) |",
37
- "| `Ctrl+D` | Exit (when editor is empty) |",
38
- "| `Ctrl+Z` | Suspend to background |",
39
- "| `Shift+Tab` | Cycle thinking level |",
40
- "| `Ctrl+P` | Cycle role models (slow/default/smol) |",
41
- "| `Shift+Ctrl+P` | Cycle role models (temporary) |",
36
+ `| \`${appKey(bindings, "interrupt")}\` | Cancel autocomplete / interrupt active work |`,
37
+ `| \`${appKey(bindings, "clear")}\` | Clear editor (first) / exit (second) |`,
38
+ `| \`${appKey(bindings, "exit")}\` | Exit (when editor is empty) |`,
39
+ `| \`${appKey(bindings, "suspend")}\` | Suspend to background |`,
40
+ `| \`${appKey(bindings, "cycleThinkingLevel")}\` | Cycle thinking level |`,
41
+ `| \`${appKey(bindings, "cycleModelForward")}\` | Cycle role models (slow/default/smol) |`,
42
+ `| \`${appKey(bindings, "cycleModelBackward")}\` | Cycle role models (temporary) |`,
42
43
  "| `Alt+P` | Select model (temporary) |",
43
- "| `Ctrl+L` | Select model (set roles) |",
44
- `| \`${planModeKey}\` | Toggle plan mode |`,
45
- "| `Ctrl+R` | Search prompt history |",
46
- `| \`${expandToolsKey}\` | Toggle tool output expansion |`,
47
- "| `Ctrl+T` | Toggle todo list expansion |",
48
- "| `Ctrl+G` | Edit message in external editor |",
49
- `| \`${sttKey}\` | Toggle speech-to-text recording |`,
44
+ `| \`${appKey(bindings, "selectModel")}\` | Select model (set roles) |`,
45
+ `| \`${appKey(bindings, "togglePlanMode")}\` | Toggle plan mode |`,
46
+ `| \`${appKey(bindings, "historySearch")}\` | Search prompt history |`,
47
+ `| \`${appKey(bindings, "expandTools")}\` | Toggle tool output expansion |`,
48
+ `| \`${appKey(bindings, "toggleThinking")}\` | Toggle thinking block visibility |`,
49
+ `| \`${appKey(bindings, "externalEditor")}\` | Edit message in external editor |`,
50
+ `| \`${appKey(bindings, "pasteImage")}\` | Paste image from clipboard |`,
51
+ `| \`${appKey(bindings, "toggleSTT")}\` | Toggle speech-to-text recording |`,
50
52
  "| `#` | Open prompt actions |",
51
53
  "| `/` | Slash commands |",
52
54
  "| `!` | Run bash command |",
@@ -25,6 +25,7 @@ import hashlineDescription from "../prompts/tools/hashline.md" with { type: "tex
25
25
  import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
26
26
  import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
27
27
  import type { ToolSession } from "../tools";
28
+ import { checkAutoGeneratedFile, checkAutoGeneratedFileContent } from "../tools/auto-generated-guard";
28
29
  import {
29
30
  invalidateFsScanAfterDelete,
30
31
  invalidateFsScanAfterRename,
@@ -135,6 +136,27 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
135
136
  });
136
137
  }
137
138
 
139
+ /**
140
+ * Strip hashline display prefixes only (no diff markers).
141
+ *
142
+ * Unlike {@link stripNewLinePrefixes} which also handles `+` diff markers,
143
+ * this only strips `LINE#ID:` / `#ID:` prefixes. Used by the write tool
144
+ * where diff markers are not applicable.
145
+ *
146
+ * Returns the original array reference when no stripping is needed.
147
+ */
148
+ export function stripHashlinePrefixes(lines: string[]): string[] {
149
+ let hashPrefixCount = 0;
150
+ let nonEmpty = 0;
151
+ for (const l of lines) {
152
+ if (l.length === 0) continue;
153
+ nonEmpty++;
154
+ if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
155
+ }
156
+ if (nonEmpty === 0 || hashPrefixCount !== nonEmpty) return lines;
157
+ return lines.map(l => l.replace(HASHLINE_PREFIX_RE, ""));
158
+ }
159
+
138
160
  export function hashlineParseText(edit: string[] | string | null): string[] {
139
161
  if (edit === null) return [];
140
162
  if (typeof edit === "string") {
@@ -546,6 +568,7 @@ export class EditTool implements AgentTool<TInput> {
546
568
  const anchorEdits = resolveEditAnchors(edits);
547
569
 
548
570
  const rawContent = await fs.readFile(absolutePath, "utf-8");
571
+ await checkAutoGeneratedFileContent(rawContent, path);
549
572
  const { bom, text } = stripBom(rawContent);
550
573
  const originalEnding = detectLineEnding(text);
551
574
  const originalNormalized = normalizeToLF(text);
@@ -685,6 +708,8 @@ export class EditTool implements AgentTool<TInput> {
685
708
  throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
686
709
  }
687
710
 
711
+ await checkAutoGeneratedFile(resolvedPath, path);
712
+
688
713
  const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
689
714
  const fs = new LspFileSystem(this.#writethrough, signal, batchRequest);
690
715
  const result = await applyPatch(input, {
package/src/sdk.ts CHANGED
@@ -68,6 +68,7 @@ import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } fro
68
68
  import {
69
69
  collectDiscoverableMCPTools,
70
70
  formatDiscoverableMCPToolServerSummary,
71
+ selectDiscoverableMCPToolNamesByServer,
71
72
  summarizeDiscoverableMCPTools,
72
73
  } from "./mcp/discoverable-tool-metadata";
73
74
  import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
@@ -421,6 +422,10 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
421
422
  label: tool.label,
422
423
  description: tool.description,
423
424
  parameters: tool.parameters,
425
+ hidden: tool.hidden,
426
+ deferrable: tool.deferrable,
427
+ mcpServerName: tool.mcpServerName,
428
+ mcpToolName: tool.mcpToolName,
424
429
  execute: (toolCallId, params, signal, onUpdate, ctx) =>
425
430
  tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
426
431
  onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
@@ -1284,15 +1289,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1284
1289
  const explicitlyRequestedMCPToolNames = options.toolNames
1285
1290
  ? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
1286
1291
  : [];
1292
+ const discoveryDefaultServers = new Set(
1293
+ (settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
1294
+ );
1295
+ const discoveryDefaultServerToolNames = mcpDiscoveryEnabled
1296
+ ? selectDiscoverableMCPToolNamesByServer(
1297
+ collectDiscoverableMCPTools(toolRegistry.values()),
1298
+ discoveryDefaultServers,
1299
+ )
1300
+ : [];
1287
1301
  let initialSelectedMCPToolNames: string[] = [];
1288
1302
  let defaultSelectedMCPToolNames: string[] = [];
1289
1303
  let initialToolNames = [...requestedActiveToolNames];
1290
1304
  if (mcpDiscoveryEnabled) {
1291
1305
  const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name => toolRegistry.has(name));
1306
+ defaultSelectedMCPToolNames = [
1307
+ ...new Set([...discoveryDefaultServerToolNames, ...explicitlyRequestedMCPToolNames]),
1308
+ ];
1292
1309
  initialSelectedMCPToolNames = existingSession.hasPersistedMCPToolSelection
1293
1310
  ? restoredSelectedMCPToolNames
1294
- : [...new Set([...restoredSelectedMCPToolNames, ...explicitlyRequestedMCPToolNames])];
1295
- defaultSelectedMCPToolNames = [...explicitlyRequestedMCPToolNames];
1311
+ : [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
1296
1312
  initialToolNames = [
1297
1313
  ...new Set([
1298
1314
  ...requestedActiveToolNames.filter(name => !name.startsWith("mcp_")),
@@ -1493,6 +1509,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1493
1509
  mcpDiscoveryEnabled,
1494
1510
  initialSelectedMCPToolNames,
1495
1511
  defaultSelectedMCPToolNames,
1512
+ defaultSelectedMCPServerNames: [...discoveryDefaultServers],
1496
1513
  ttsrManager,
1497
1514
  obfuscator,
1498
1515
  asyncJobManager,
@@ -93,6 +93,7 @@ import {
93
93
  type DiscoverableMCPSearchIndex,
94
94
  type DiscoverableMCPTool,
95
95
  isMCPToolName,
96
+ selectDiscoverableMCPToolNamesByServer,
96
97
  } from "../mcp/discoverable-tool-metadata";
97
98
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
98
99
  import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
@@ -223,6 +224,8 @@ export interface AgentSessionConfig {
223
224
  mcpDiscoveryEnabled?: boolean;
224
225
  /** MCP tool names to activate for the current session when discovery mode is enabled. */
225
226
  initialSelectedMCPToolNames?: string[];
227
+ /** MCP server names whose tools should seed discovery-mode sessions whenever those servers are connected. */
228
+ defaultSelectedMCPServerNames?: string[];
226
229
  /** MCP tool names that should seed brand-new sessions created from this AgentSession. */
227
230
  defaultSelectedMCPToolNames?: string[];
228
231
  /** TTSR manager for time-traveling stream rules */
@@ -423,6 +426,7 @@ export class AgentSession {
423
426
  #discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
424
427
  #discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
425
428
  #selectedMCPToolNames = new Set<string>();
429
+ #defaultSelectedMCPServerNames = new Set<string>();
426
430
  #defaultSelectedMCPToolNames = new Set<string>();
427
431
  #sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
428
432
 
@@ -474,6 +478,7 @@ export class AgentSession {
474
478
  this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
475
479
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
476
480
  this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
481
+ this.#defaultSelectedMCPServerNames = new Set(config.defaultSelectedMCPServerNames ?? []);
477
482
  this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
478
483
  this.#pruneSelectedMCPToolNames();
479
484
  const persistedSelectedMCPToolNames = this.sessionManager.buildSessionContext().selectedMCPToolNames;
@@ -486,7 +491,7 @@ export class AgentSession {
486
491
  }
487
492
  this.#rememberSessionDefaultSelectedMCPToolNames(
488
493
  this.sessionManager.getSessionFile(),
489
- this.#defaultSelectedMCPToolNames,
494
+ this.#getConfiguredDefaultSelectedMCPToolNames(),
490
495
  );
491
496
  this.#ttsrManager = config.ttsrManager;
492
497
  this.#obfuscator = config.obfuscator;
@@ -1659,6 +1664,16 @@ export class AgentSession {
1659
1664
  return Array.from(toolNames).filter(name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name));
1660
1665
  }
1661
1666
 
1667
+ #getConfiguredDefaultSelectedMCPToolNames(): string[] {
1668
+ return this.#filterSelectableMCPToolNames([
1669
+ ...this.#defaultSelectedMCPToolNames,
1670
+ ...selectDiscoverableMCPToolNamesByServer(
1671
+ this.#discoverableMCPTools.values(),
1672
+ this.#defaultSelectedMCPServerNames,
1673
+ ),
1674
+ ]);
1675
+ }
1676
+
1662
1677
  #pruneSelectedMCPToolNames(): void {
1663
1678
  this.#selectedMCPToolNames = new Set(this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames));
1664
1679
  }
@@ -1815,11 +1830,15 @@ export class AgentSession {
1815
1830
  ): Promise<void> {
1816
1831
  if (!this.#mcpDiscoveryEnabled) return;
1817
1832
  const nextActiveNonMCPToolNames = this.#getActiveNonMCPToolNames();
1818
- const fallbackSelectedMCPToolNames = options?.fallbackSelectedMCPToolNames ?? this.#defaultSelectedMCPToolNames;
1833
+ const fallbackSelectedMCPToolNames =
1834
+ options?.fallbackSelectedMCPToolNames ?? this.#getConfiguredDefaultSelectedMCPToolNames();
1819
1835
  const restoredMCPToolNames = sessionContext.hasPersistedMCPToolSelection
1820
1836
  ? this.#filterSelectableMCPToolNames(sessionContext.selectedMCPToolNames)
1821
1837
  : this.#filterSelectableMCPToolNames(fallbackSelectedMCPToolNames);
1822
- this.#rememberSessionDefaultSelectedMCPToolNames(this.sessionFile, restoredMCPToolNames);
1838
+ this.#rememberSessionDefaultSelectedMCPToolNames(
1839
+ this.sessionFile,
1840
+ this.#getConfiguredDefaultSelectedMCPToolNames(),
1841
+ );
1823
1842
  await this.#applyActiveToolsByName([...nextActiveNonMCPToolNames, ...restoredMCPToolNames], {
1824
1843
  persistMCPSelection: false,
1825
1844
  });
@@ -1866,6 +1885,16 @@ export class AgentSession {
1866
1885
 
1867
1886
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
1868
1887
  this.#pruneSelectedMCPToolNames();
1888
+ if (!this.sessionManager.buildSessionContext().hasPersistedMCPToolSelection) {
1889
+ this.#selectedMCPToolNames = new Set([
1890
+ ...this.#selectedMCPToolNames,
1891
+ ...this.#getConfiguredDefaultSelectedMCPToolNames(),
1892
+ ]);
1893
+ }
1894
+ this.#rememberSessionDefaultSelectedMCPToolNames(
1895
+ this.sessionFile,
1896
+ this.#getConfiguredDefaultSelectedMCPToolNames(),
1897
+ );
1869
1898
 
1870
1899
  const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()];
1871
1900
  await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
@@ -2858,7 +2887,10 @@ export class AgentSession {
2858
2887
  this.sessionManager.appendMCPToolSelection(this.getSelectedMCPToolNames());
2859
2888
  }
2860
2889
  }
2861
- this.#rememberSessionDefaultSelectedMCPToolNames(this.sessionFile, this.#defaultSelectedMCPToolNames);
2890
+ this.#rememberSessionDefaultSelectedMCPToolNames(
2891
+ this.sessionFile,
2892
+ this.#getConfiguredDefaultSelectedMCPToolNames(),
2893
+ );
2862
2894
 
2863
2895
  this.#todoReminderCount = 0;
2864
2896
  this.#planReferenceSent = false;
@@ -5515,7 +5547,7 @@ export class AgentSession {
5515
5547
  const model = this.agent.state.model;
5516
5548
  const thinkingLevel = this.#thinkingLevel;
5517
5549
  lines.push("## Configuration\n");
5518
- lines.push(`Model: ${model.provider}/${model.id}`);
5550
+ lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
5519
5551
  lines.push(`Thinking Level: ${thinkingLevel}`);
5520
5552
  lines.push("\n");
5521
5553
 
@@ -775,7 +775,7 @@ function buildOpenAiNativeHistory(
775
775
  continue;
776
776
  }
777
777
 
778
- if (block.type === "toolCall" && assistant.stopReason !== "error") {
778
+ if (block.type === "toolCall") {
779
779
  const normalized = normalizeResponsesToolCallId(block.id);
780
780
  let itemId: string | undefined = normalized.itemId;
781
781
  if (isDifferentModel && (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_"))) {