@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.
- package/CHANGELOG.md +26 -1
- package/README.md +3 -1
- package/package.json +7 -7
- package/src/capability/mcp.ts +2 -0
- package/src/config/keybindings.ts +0 -4
- package/src/config/mcp-schema.json +230 -0
- package/src/config/settings-schema.ts +30 -1
- package/src/discovery/builtin.ts +1 -0
- package/src/discovery/mcp-json.ts +3 -0
- package/src/extensibility/custom-tools/types.ts +4 -0
- package/src/extensibility/extensions/types.ts +4 -0
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/main.ts +6 -0
- package/src/mcp/client.ts +46 -16
- package/src/mcp/config-writer.ts +9 -2
- package/src/mcp/config.ts +1 -0
- package/src/mcp/discoverable-tool-metadata.ts +10 -0
- package/src/mcp/manager.ts +28 -0
- package/src/mcp/transports/http.ts +148 -28
- package/src/mcp/transports/stdio.ts +62 -12
- package/src/mcp/types.ts +22 -1
- package/src/modes/components/custom-editor.ts +126 -71
- package/src/modes/controllers/command-controller.ts +3 -12
- package/src/modes/controllers/extension-ui-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +36 -37
- package/src/modes/controllers/selector-controller.ts +11 -0
- package/src/modes/interactive-mode.ts +5 -12
- package/src/modes/utils/hotkeys-markdown.ts +24 -22
- package/src/patch/index.ts +25 -0
- package/src/sdk.ts +19 -2
- package/src/session/agent-session.ts +37 -5
- package/src/session/compaction/compaction.ts +1 -1
- package/src/system-prompt.ts +26 -14
- package/src/tools/auto-generated-guard.ts +310 -0
- package/src/tools/write.ts +33 -2
- 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
|
|
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,
|
|
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-
|
|
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.
|
|
86
|
-
this.ctx.editor.
|
|
87
|
-
this.ctx.editor.
|
|
88
|
-
this.ctx.editor.
|
|
89
|
-
this.ctx.editor.
|
|
90
|
-
this.ctx.editor.
|
|
91
|
-
this.ctx.editor.
|
|
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.
|
|
96
|
-
this.ctx.editor.
|
|
97
|
-
this.ctx.editor.
|
|
98
|
-
this.ctx.editor.
|
|
99
|
-
this.ctx.editor.
|
|
100
|
-
this.ctx.editor.
|
|
101
|
-
|
|
102
|
-
this.ctx.editor.
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
`| \`${
|
|
29
|
-
`| \`${
|
|
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
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
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
|
-
"
|
|
44
|
-
`| \`${
|
|
45
|
-
"
|
|
46
|
-
`| \`${
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
`| \`${
|
|
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 |",
|
package/src/patch/index.ts
CHANGED
|
@@ -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, ...
|
|
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.#
|
|
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 =
|
|
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(
|
|
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(
|
|
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"
|
|
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_"))) {
|