@oh-my-pi/pi-coding-agent 13.13.0 → 13.14.0
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 +39 -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/model-registry.ts +10 -3
- package/src/config/settings-schema.ts +13 -0
- package/src/discovery/builtin.ts +1 -0
- package/src/discovery/helpers.ts +9 -2
- 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/mcp/client.ts +72 -17
- 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 +284 -57
- package/src/mcp/tool-bridge.ts +189 -106
- package/src/mcp/transports/http.ts +154 -29
- 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/mcp-command-controller.ts +45 -0
- package/src/modes/controllers/selector-controller.ts +11 -0
- package/src/modes/interactive-mode.ts +14 -17
- package/src/modes/utils/hotkeys-markdown.ts +24 -22
- package/src/patch/index.ts +36 -7
- package/src/prompts/agents/explore.md +4 -67
- package/src/sdk.ts +19 -2
- package/src/session/agent-session.ts +44 -21
- package/src/session/compaction/compaction.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +26 -14
- package/src/tools/write.ts +26 -2
- package/src/utils/title-generator.ts +46 -3
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import { getProjectDir, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { type Subprocess, spawn } from "bun";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
JsonRpcError,
|
|
12
|
+
JsonRpcMessage,
|
|
13
|
+
JsonRpcRequest,
|
|
14
|
+
JsonRpcResponse,
|
|
15
|
+
MCPRequestOptions,
|
|
16
|
+
MCPStdioServerConfig,
|
|
17
|
+
MCPTransport,
|
|
18
|
+
} from "../../mcp/types";
|
|
19
|
+
import { toJsonRpcError } from "../../mcp/types";
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
22
|
* Stdio transport for MCP servers.
|
|
@@ -28,6 +37,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
28
37
|
onClose?: () => void;
|
|
29
38
|
onError?: (error: Error) => void;
|
|
30
39
|
onNotification?: (method: string, params: unknown) => void;
|
|
40
|
+
onRequest?: (method: string, params: unknown) => Promise<unknown>;
|
|
31
41
|
|
|
32
42
|
constructor(private config: MCPStdioServerConfig) {}
|
|
33
43
|
|
|
@@ -71,7 +81,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
71
81
|
for await (const line of readJsonl(this.#process.stdout)) {
|
|
72
82
|
if (!this.#connected) break;
|
|
73
83
|
try {
|
|
74
|
-
this.#handleMessage(line as
|
|
84
|
+
this.#handleMessage(line as JsonRpcMessage);
|
|
75
85
|
} catch {
|
|
76
86
|
// Skip malformed lines
|
|
77
87
|
}
|
|
@@ -109,25 +119,65 @@ export class StdioTransport implements MCPTransport {
|
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
#handleMessage(message:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
#handleMessage(message: JsonRpcMessage | JsonRpcMessage[]): void {
|
|
123
|
+
if (Array.isArray(message)) {
|
|
124
|
+
for (const m of message) this.#handleMessage(m);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Server-to-client request: has both method and id
|
|
128
|
+
if ("method" in message && "id" in message && message.id != null) {
|
|
129
|
+
void this.#handleServerRequest(message as JsonRpcRequest);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Response to our request: has id
|
|
134
|
+
if ("id" in message && message.id != null) {
|
|
135
|
+
const response = message as JsonRpcResponse;
|
|
136
|
+
const pending = this.#pendingRequests.get(response.id);
|
|
116
137
|
if (pending) {
|
|
117
|
-
this.#pendingRequests.delete(
|
|
118
|
-
if (
|
|
119
|
-
pending.reject(new Error(`MCP error ${
|
|
138
|
+
this.#pendingRequests.delete(response.id);
|
|
139
|
+
if (response.error) {
|
|
140
|
+
pending.reject(new Error(`MCP error ${response.error.code}: ${response.error.message}`));
|
|
120
141
|
} else {
|
|
121
|
-
pending.resolve(
|
|
142
|
+
pending.resolve(response.result);
|
|
122
143
|
}
|
|
123
144
|
}
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Notification: has method but no id
|
|
149
|
+
if ("method" in message) {
|
|
126
150
|
const notification = message as { method: string; params?: unknown };
|
|
127
151
|
this.onNotification?.(notification.method, notification.params);
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
async #handleServerRequest(request: JsonRpcRequest): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
if (!this.onRequest) {
|
|
158
|
+
this.#sendResponse(request.id, undefined, { code: -32601, message: "Method not found" });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const result = await this.onRequest(request.method, request.params);
|
|
162
|
+
this.#sendResponse(request.id, result);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
try {
|
|
165
|
+
this.#sendResponse(request.id, undefined, toJsonRpcError(error));
|
|
166
|
+
} catch {
|
|
167
|
+
// Best-effort — process may have exited
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#sendResponse(id: string | number, result?: unknown, error?: JsonRpcError): void {
|
|
173
|
+
if (!this.#connected || !this.#process?.stdin) return;
|
|
174
|
+
const response = error
|
|
175
|
+
? { jsonrpc: "2.0" as const, id, error }
|
|
176
|
+
: { jsonrpc: "2.0" as const, id, result: result ?? {} };
|
|
177
|
+
this.#process.stdin.write(`${JSON.stringify(response)}\n`);
|
|
178
|
+
this.#process.stdin.flush();
|
|
179
|
+
}
|
|
180
|
+
|
|
131
181
|
#handleClose(): void {
|
|
132
182
|
if (!this.#connected) return;
|
|
133
183
|
this.#connected = false;
|
package/src/mcp/types.ts
CHANGED
|
@@ -100,8 +100,12 @@ export interface MCPSseServerConfig extends MCPServerConfigBase {
|
|
|
100
100
|
|
|
101
101
|
export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSseServerConfig;
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
export const MCP_CONFIG_SCHEMA_URL =
|
|
104
|
+
"https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json";
|
|
105
|
+
|
|
106
|
+
/** Root mcp.json/.mcp.json file structure */
|
|
104
107
|
export interface MCPConfigFile {
|
|
108
|
+
$schema?: string;
|
|
105
109
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
106
110
|
disabledServers?: string[];
|
|
107
111
|
}
|
|
@@ -228,6 +232,8 @@ export interface MCPTransport {
|
|
|
228
232
|
onClose?: () => void;
|
|
229
233
|
onError?: (error: Error) => void;
|
|
230
234
|
onNotification?: (method: string, params: unknown) => void;
|
|
235
|
+
/** Handler for server-to-client requests (e.g. roots/list). Returns result or throws a JsonRpcError. */
|
|
236
|
+
onRequest?: (method: string, params: unknown) => Promise<unknown>;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
/** Transport factory function */
|
|
@@ -400,3 +406,18 @@ export const MCPNotificationMethods = {
|
|
|
400
406
|
RESOURCES_UPDATED: "notifications/resources/updated",
|
|
401
407
|
PROMPTS_LIST_CHANGED: "notifications/prompts/list_changed",
|
|
402
408
|
} as const;
|
|
409
|
+
|
|
410
|
+
/** Extract a JsonRpcError from a thrown value. Preserves `.code` and `.message` from Error instances or plain objects. */
|
|
411
|
+
export function toJsonRpcError(error: unknown): JsonRpcError {
|
|
412
|
+
if (error instanceof Error) {
|
|
413
|
+
const code = "code" in error && typeof error.code === "number" ? error.code : -32603;
|
|
414
|
+
return { code, message: error.message };
|
|
415
|
+
}
|
|
416
|
+
if (typeof error === "object" && error !== null) {
|
|
417
|
+
const obj = error as Record<string, unknown>;
|
|
418
|
+
if (typeof obj.code === "number" && typeof obj.message === "string") {
|
|
419
|
+
return { code: obj.code, message: obj.message };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { code: -32603, message: "Internal error" };
|
|
423
|
+
}
|
|
@@ -1,34 +1,89 @@
|
|
|
1
1
|
import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { AppAction } from "../../config/keybindings";
|
|
3
|
+
|
|
4
|
+
type ConfigurableEditorAction = Extract<
|
|
5
|
+
AppAction,
|
|
6
|
+
| "interrupt"
|
|
7
|
+
| "clear"
|
|
8
|
+
| "exit"
|
|
9
|
+
| "suspend"
|
|
10
|
+
| "cycleThinkingLevel"
|
|
11
|
+
| "cycleModelForward"
|
|
12
|
+
| "cycleModelBackward"
|
|
13
|
+
| "selectModel"
|
|
14
|
+
| "expandTools"
|
|
15
|
+
| "toggleThinking"
|
|
16
|
+
| "externalEditor"
|
|
17
|
+
| "historySearch"
|
|
18
|
+
| "dequeue"
|
|
19
|
+
| "pasteImage"
|
|
20
|
+
| "copyPrompt"
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
24
|
+
interrupt: ["escape"],
|
|
25
|
+
clear: ["ctrl+c"],
|
|
26
|
+
exit: ["ctrl+d"],
|
|
27
|
+
suspend: ["ctrl+z"],
|
|
28
|
+
cycleThinkingLevel: ["shift+tab"],
|
|
29
|
+
cycleModelForward: ["ctrl+p"],
|
|
30
|
+
cycleModelBackward: ["shift+ctrl+p"],
|
|
31
|
+
selectModel: ["ctrl+l"],
|
|
32
|
+
expandTools: ["ctrl+o"],
|
|
33
|
+
toggleThinking: ["ctrl+t"],
|
|
34
|
+
externalEditor: ["ctrl+g"],
|
|
35
|
+
historySearch: ["ctrl+r"],
|
|
36
|
+
dequeue: ["alt+up"],
|
|
37
|
+
pasteImage: ["ctrl+v"],
|
|
38
|
+
copyPrompt: ["alt+shift+c"],
|
|
39
|
+
};
|
|
2
40
|
|
|
3
41
|
/**
|
|
4
|
-
* Custom editor that handles
|
|
42
|
+
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
|
5
43
|
*/
|
|
6
44
|
export class CustomEditor extends Editor {
|
|
7
45
|
onEscape?: () => void;
|
|
8
46
|
shouldBypassAutocompleteOnEscape?: () => boolean;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/** Called when Alt+Shift+C is pressed to copy prompt to clipboard. */
|
|
47
|
+
onClear?: () => void;
|
|
48
|
+
onExit?: () => void;
|
|
49
|
+
onCycleThinkingLevel?: () => void;
|
|
50
|
+
onCycleModelForward?: () => void;
|
|
51
|
+
onCycleModelBackward?: () => void;
|
|
52
|
+
onSelectModel?: () => void;
|
|
53
|
+
onExpandTools?: () => void;
|
|
54
|
+
onToggleThinking?: () => void;
|
|
55
|
+
onExternalEditor?: () => void;
|
|
56
|
+
onHistorySearch?: () => void;
|
|
57
|
+
onSuspend?: () => void;
|
|
58
|
+
onShowHotkeys?: () => void;
|
|
59
|
+
onQuickSelectModel?: () => void;
|
|
60
|
+
/** Called when the configured copy-prompt shortcut is pressed. */
|
|
24
61
|
onCopyPrompt?: () => void;
|
|
25
|
-
/** Called when
|
|
26
|
-
|
|
27
|
-
/** Called when
|
|
28
|
-
|
|
62
|
+
/** Called when the configured image-paste shortcut is pressed. */
|
|
63
|
+
onPasteImage?: () => Promise<boolean>;
|
|
64
|
+
/** Called when the configured dequeue shortcut is pressed. */
|
|
65
|
+
onDequeue?: () => void;
|
|
66
|
+
/** Called when Caps Lock is pressed. */
|
|
67
|
+
onCapsLock?: () => void;
|
|
29
68
|
|
|
30
|
-
/** Custom key handlers from extensions */
|
|
69
|
+
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
31
70
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
71
|
+
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
72
|
+
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
|
|
76
|
+
this.#actionKeys.set(action, [...keys]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#matchesAction(data: string, action: ConfigurableEditorAction): boolean {
|
|
80
|
+
const keys = this.#actionKeys.get(action);
|
|
81
|
+
if (!keys) return false;
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
if (matchesKey(data, key)) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
32
87
|
|
|
33
88
|
/**
|
|
34
89
|
* Register a custom key handler. Extensions use this for shortcuts.
|
|
@@ -59,111 +114,111 @@ export class CustomEditor extends Editor {
|
|
|
59
114
|
return;
|
|
60
115
|
}
|
|
61
116
|
|
|
62
|
-
// Intercept
|
|
63
|
-
if (
|
|
64
|
-
void this.
|
|
117
|
+
// Intercept configured image paste (async - fires and handles result)
|
|
118
|
+
if (this.#matchesAction(data, "pasteImage") && this.onPasteImage) {
|
|
119
|
+
void this.onPasteImage();
|
|
65
120
|
return;
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
// Intercept
|
|
69
|
-
if (
|
|
70
|
-
this.
|
|
123
|
+
// Intercept configured external editor shortcut
|
|
124
|
+
if (this.#matchesAction(data, "externalEditor") && this.onExternalEditor) {
|
|
125
|
+
this.onExternalEditor();
|
|
71
126
|
return;
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
// Intercept Alt+P for quick model switching
|
|
75
|
-
if (matchesKey(data, "alt+p") && this.
|
|
76
|
-
this.
|
|
130
|
+
if (matchesKey(data, "alt+p") && this.onQuickSelectModel) {
|
|
131
|
+
this.onQuickSelectModel();
|
|
77
132
|
return;
|
|
78
133
|
}
|
|
79
134
|
|
|
80
|
-
// Intercept
|
|
81
|
-
if (
|
|
82
|
-
this.
|
|
135
|
+
// Intercept configured suspend shortcut
|
|
136
|
+
if (this.#matchesAction(data, "suspend") && this.onSuspend) {
|
|
137
|
+
this.onSuspend();
|
|
83
138
|
return;
|
|
84
139
|
}
|
|
85
140
|
|
|
86
|
-
// Intercept
|
|
87
|
-
if (
|
|
88
|
-
this.
|
|
141
|
+
// Intercept configured thinking block visibility toggle
|
|
142
|
+
if (this.#matchesAction(data, "toggleThinking") && this.onToggleThinking) {
|
|
143
|
+
this.onToggleThinking();
|
|
89
144
|
return;
|
|
90
145
|
}
|
|
91
146
|
|
|
92
|
-
// Intercept
|
|
93
|
-
if (
|
|
94
|
-
this.
|
|
147
|
+
// Intercept configured model selector shortcut
|
|
148
|
+
if (this.#matchesAction(data, "selectModel") && this.onSelectModel) {
|
|
149
|
+
this.onSelectModel();
|
|
95
150
|
return;
|
|
96
151
|
}
|
|
97
152
|
|
|
98
|
-
// Intercept
|
|
99
|
-
if (
|
|
100
|
-
this.
|
|
153
|
+
// Intercept configured history search shortcut
|
|
154
|
+
if (this.#matchesAction(data, "historySearch") && this.onHistorySearch) {
|
|
155
|
+
this.onHistorySearch();
|
|
101
156
|
return;
|
|
102
157
|
}
|
|
103
158
|
|
|
104
|
-
// Intercept
|
|
105
|
-
if (
|
|
106
|
-
this.
|
|
159
|
+
// Intercept configured tool output expansion shortcut
|
|
160
|
+
if (this.#matchesAction(data, "expandTools") && this.onExpandTools) {
|
|
161
|
+
this.onExpandTools();
|
|
107
162
|
return;
|
|
108
163
|
}
|
|
109
164
|
|
|
110
|
-
// Intercept
|
|
111
|
-
if ((
|
|
112
|
-
this.
|
|
165
|
+
// Intercept configured backward model cycling (check before forward cycling)
|
|
166
|
+
if (this.#matchesAction(data, "cycleModelBackward") && this.onCycleModelBackward) {
|
|
167
|
+
this.onCycleModelBackward();
|
|
113
168
|
return;
|
|
114
169
|
}
|
|
115
170
|
|
|
116
|
-
// Intercept
|
|
117
|
-
if (
|
|
118
|
-
this.
|
|
171
|
+
// Intercept configured forward model cycling
|
|
172
|
+
if (this.#matchesAction(data, "cycleModelForward") && this.onCycleModelForward) {
|
|
173
|
+
this.onCycleModelForward();
|
|
119
174
|
return;
|
|
120
175
|
}
|
|
121
176
|
|
|
122
|
-
// Intercept
|
|
123
|
-
if (
|
|
124
|
-
this.
|
|
177
|
+
// Intercept configured thinking level cycling
|
|
178
|
+
if (this.#matchesAction(data, "cycleThinkingLevel") && this.onCycleThinkingLevel) {
|
|
179
|
+
this.onCycleThinkingLevel();
|
|
125
180
|
return;
|
|
126
181
|
}
|
|
127
182
|
|
|
128
|
-
// Intercept
|
|
129
|
-
// Default behavior keeps autocomplete dismissal, but parent can prioritize global
|
|
130
|
-
if ((
|
|
183
|
+
// Intercept configured interrupt shortcut.
|
|
184
|
+
// Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling.
|
|
185
|
+
if (this.#matchesAction(data, "interrupt") && this.onEscape) {
|
|
131
186
|
if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
|
|
132
187
|
this.onEscape();
|
|
133
188
|
return;
|
|
134
189
|
}
|
|
135
190
|
}
|
|
136
191
|
|
|
137
|
-
// Intercept
|
|
138
|
-
if (
|
|
139
|
-
this.
|
|
192
|
+
// Intercept configured clear shortcut
|
|
193
|
+
if (this.#matchesAction(data, "clear") && this.onClear) {
|
|
194
|
+
this.onClear();
|
|
140
195
|
return;
|
|
141
196
|
}
|
|
142
197
|
|
|
143
|
-
// Intercept
|
|
144
|
-
if (
|
|
145
|
-
if (this.getText().length === 0 && this.
|
|
146
|
-
this.
|
|
198
|
+
// Intercept configured exit shortcut (only when editor is empty)
|
|
199
|
+
if (this.#matchesAction(data, "exit")) {
|
|
200
|
+
if (this.getText().length === 0 && this.onExit) {
|
|
201
|
+
this.onExit();
|
|
147
202
|
}
|
|
148
|
-
// Always consume
|
|
203
|
+
// Always consume exit shortcut (don't pass to parent)
|
|
149
204
|
return;
|
|
150
205
|
}
|
|
151
206
|
|
|
152
|
-
// Intercept
|
|
153
|
-
if (
|
|
154
|
-
this.
|
|
207
|
+
// Intercept configured dequeue shortcut (restore queued message to editor)
|
|
208
|
+
if (this.#matchesAction(data, "dequeue") && this.onDequeue) {
|
|
209
|
+
this.onDequeue();
|
|
155
210
|
return;
|
|
156
211
|
}
|
|
157
212
|
|
|
158
|
-
// Intercept
|
|
159
|
-
if (
|
|
213
|
+
// Intercept configured copy-prompt shortcut
|
|
214
|
+
if (this.#matchesAction(data, "copyPrompt") && this.onCopyPrompt) {
|
|
160
215
|
this.onCopyPrompt();
|
|
161
216
|
return;
|
|
162
217
|
}
|
|
163
218
|
|
|
164
219
|
// Intercept ? when editor is empty to show hotkeys
|
|
165
|
-
if (data === "?" && this.getText().length === 0 && this.
|
|
166
|
-
this.
|
|
220
|
+
if (data === "?" && this.getText().length === 0 && this.onShowHotkeys) {
|
|
221
|
+
this.onShowHotkeys();
|
|
167
222
|
return;
|
|
168
223
|
}
|
|
169
224
|
|
|
@@ -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(() => {});
|