@oh-my-pi/pi-coding-agent 12.7.6 → 12.8.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 +37 -37
- package/README.md +9 -1052
- package/package.json +7 -7
- package/src/cli/args.ts +1 -0
- package/src/cli/update-cli.ts +49 -35
- package/src/cli/web-search-cli.ts +3 -2
- package/src/commands/web-search.ts +1 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/settings-schema.ts +25 -3
- package/src/config/settings.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +20 -13
- package/src/extensibility/slash-commands.ts +12 -91
- package/src/lsp/client.ts +24 -27
- package/src/lsp/index.ts +92 -42
- package/src/mcp/config-writer.ts +33 -0
- package/src/mcp/config.ts +6 -1
- package/src/mcp/types.ts +1 -0
- package/src/modes/components/custom-editor.ts +8 -5
- package/src/modes/components/settings-defs.ts +2 -1
- package/src/modes/controllers/command-controller.ts +12 -6
- package/src/modes/controllers/input-controller.ts +21 -186
- package/src/modes/controllers/mcp-command-controller.ts +60 -3
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/sdk.ts +23 -1
- package/src/secrets/index.ts +116 -0
- package/src/secrets/obfuscator.ts +269 -0
- package/src/secrets/regex.ts +21 -0
- package/src/session/agent-session.ts +143 -21
- package/src/session/compaction/branch-summarization.ts +2 -2
- package/src/session/compaction/compaction.ts +10 -3
- package/src/session/compaction/utils.ts +25 -1
- package/src/slash-commands/builtin-registry.ts +419 -0
- package/src/web/scrapers/github.ts +50 -12
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +13 -2
- package/src/web/search/providers/brave.ts +165 -0
- package/src/web/search/types.ts +1 -1
- package/docs/compaction.md +0 -436
- package/docs/config-usage.md +0 -176
- package/docs/custom-tools.md +0 -585
- package/docs/environment-variables.md +0 -257
- package/docs/extension-loading.md +0 -106
- package/docs/extensions.md +0 -1342
- package/docs/fs-scan-cache-architecture.md +0 -50
- package/docs/hooks.md +0 -906
- package/docs/models.md +0 -234
- package/docs/python-repl.md +0 -110
- package/docs/rpc.md +0 -1173
- package/docs/sdk.md +0 -1039
- package/docs/session-tree-plan.md +0 -84
- package/docs/session.md +0 -368
- package/docs/skills.md +0 -254
- package/docs/theme.md +0 -696
- package/docs/tree.md +0 -206
- package/docs/tui.md +0 -487
package/src/lsp/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEnoent, logger, ptree } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { isEnoent, logger, ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
3
3
|
import { applyWorkspaceEdit } from "./edits";
|
|
4
4
|
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
|
|
@@ -157,8 +157,8 @@ const CLIENT_CAPABILITIES = {
|
|
|
157
157
|
* Returns the parsed message and remaining buffer, or null if incomplete.
|
|
158
158
|
*/
|
|
159
159
|
function parseMessage(
|
|
160
|
-
buffer:
|
|
161
|
-
): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining:
|
|
160
|
+
buffer: Buffer,
|
|
161
|
+
): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining: Buffer } | null {
|
|
162
162
|
// Only decode enough to find the header
|
|
163
163
|
const headerEndIndex = findHeaderEnd(buffer);
|
|
164
164
|
if (headerEndIndex === -1) return null;
|
|
@@ -173,9 +173,9 @@ function parseMessage(
|
|
|
173
173
|
|
|
174
174
|
if (buffer.length < messageEnd) return null;
|
|
175
175
|
|
|
176
|
-
const messageBytes = buffer.
|
|
176
|
+
const messageBytes = buffer.subarray(messageStart, messageEnd);
|
|
177
177
|
const messageText = new TextDecoder().decode(messageBytes);
|
|
178
|
-
const remaining = buffer.
|
|
178
|
+
const remaining = buffer.subarray(messageEnd);
|
|
179
179
|
|
|
180
180
|
return {
|
|
181
181
|
message: JSON.parse(messageText),
|
|
@@ -195,26 +195,13 @@ function findHeaderEnd(buffer: Uint8Array): number {
|
|
|
195
195
|
return -1;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/**
|
|
199
|
-
* Concatenate two Uint8Arrays efficiently
|
|
200
|
-
*/
|
|
201
|
-
function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
202
|
-
const result = new Uint8Array(a.length + b.length);
|
|
203
|
-
result.set(a);
|
|
204
|
-
result.set(b, a.length);
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
198
|
async function writeMessage(
|
|
209
199
|
sink: Bun.FileSink,
|
|
210
200
|
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
211
201
|
): Promise<void> {
|
|
212
202
|
const content = JSON.stringify(message);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const fullMessage = new TextEncoder().encode(header + content);
|
|
216
|
-
|
|
217
|
-
sink.write(fullMessage);
|
|
203
|
+
sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
|
204
|
+
sink.write(content);
|
|
218
205
|
await sink.flush();
|
|
219
206
|
}
|
|
220
207
|
|
|
@@ -238,7 +225,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
238
225
|
if (done) break;
|
|
239
226
|
|
|
240
227
|
// Atomically update buffer before processing
|
|
241
|
-
const currentBuffer =
|
|
228
|
+
const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, value]);
|
|
242
229
|
client.messageBuffer = currentBuffer;
|
|
243
230
|
|
|
244
231
|
// Process all complete messages in buffer
|
|
@@ -499,7 +486,8 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
499
486
|
* Ensure a file is opened in the LSP client.
|
|
500
487
|
* Sends didOpen notification if the file is not already tracked.
|
|
501
488
|
*/
|
|
502
|
-
export async function ensureFileOpen(client: LspClient, filePath: string): Promise<void> {
|
|
489
|
+
export async function ensureFileOpen(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
|
|
490
|
+
throwIfAborted(signal);
|
|
503
491
|
const uri = fileToUri(filePath);
|
|
504
492
|
const lockKey = `${client.name}:${uri}`;
|
|
505
493
|
|
|
@@ -511,12 +499,13 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
511
499
|
// Check if another operation is already opening this file
|
|
512
500
|
const existingLock = fileOperationLocks.get(lockKey);
|
|
513
501
|
if (existingLock) {
|
|
514
|
-
await existingLock;
|
|
502
|
+
await untilAborted(signal, () => existingLock);
|
|
515
503
|
return;
|
|
516
504
|
}
|
|
517
505
|
|
|
518
506
|
// Lock and open file
|
|
519
507
|
const openPromise = (async () => {
|
|
508
|
+
throwIfAborted(signal);
|
|
520
509
|
// Double-check after acquiring lock
|
|
521
510
|
if (client.openFiles.has(uri)) {
|
|
522
511
|
return;
|
|
@@ -525,11 +514,13 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
525
514
|
let content: string;
|
|
526
515
|
try {
|
|
527
516
|
content = await Bun.file(filePath).text();
|
|
517
|
+
throwIfAborted(signal);
|
|
528
518
|
} catch (err) {
|
|
529
519
|
if (isEnoent(err)) return;
|
|
530
520
|
throw err;
|
|
531
521
|
}
|
|
532
522
|
const languageId = detectLanguageId(filePath);
|
|
523
|
+
throwIfAborted(signal);
|
|
533
524
|
|
|
534
525
|
await sendNotification(client, "textDocument/didOpen", {
|
|
535
526
|
textDocument: {
|
|
@@ -568,7 +559,7 @@ export async function syncContent(
|
|
|
568
559
|
|
|
569
560
|
const existingLock = fileOperationLocks.get(lockKey);
|
|
570
561
|
if (existingLock) {
|
|
571
|
-
await existingLock;
|
|
562
|
+
await untilAborted(signal, () => existingLock);
|
|
572
563
|
}
|
|
573
564
|
|
|
574
565
|
const syncPromise = (async () => {
|
|
@@ -631,38 +622,43 @@ export async function notifySaved(client: LspClient, filePath: string, signal?:
|
|
|
631
622
|
* Refresh a file in the LSP client.
|
|
632
623
|
* Increments version, sends didChange and didSave notifications.
|
|
633
624
|
*/
|
|
634
|
-
export async function refreshFile(client: LspClient, filePath: string): Promise<void> {
|
|
625
|
+
export async function refreshFile(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
|
|
626
|
+
throwIfAborted(signal);
|
|
635
627
|
const uri = fileToUri(filePath);
|
|
636
628
|
const lockKey = `${client.name}:${uri}`;
|
|
637
629
|
|
|
638
630
|
// Check if another operation is in progress
|
|
639
631
|
const existingLock = fileOperationLocks.get(lockKey);
|
|
640
632
|
if (existingLock) {
|
|
641
|
-
await existingLock;
|
|
633
|
+
await untilAborted(signal, () => existingLock);
|
|
642
634
|
}
|
|
643
635
|
|
|
644
636
|
// Lock and refresh file
|
|
645
637
|
const refreshPromise = (async () => {
|
|
638
|
+
throwIfAborted(signal);
|
|
646
639
|
const info = client.openFiles.get(uri);
|
|
647
640
|
|
|
648
641
|
if (!info) {
|
|
649
|
-
await ensureFileOpen(client, filePath);
|
|
642
|
+
await ensureFileOpen(client, filePath, signal);
|
|
650
643
|
return;
|
|
651
644
|
}
|
|
652
645
|
|
|
653
646
|
let content: string;
|
|
654
647
|
try {
|
|
655
648
|
content = await Bun.file(filePath).text();
|
|
649
|
+
throwIfAborted(signal);
|
|
656
650
|
} catch (err) {
|
|
657
651
|
if (isEnoent(err)) return;
|
|
658
652
|
throw err;
|
|
659
653
|
}
|
|
660
654
|
const version = ++info.version;
|
|
655
|
+
throwIfAborted(signal);
|
|
661
656
|
|
|
662
657
|
await sendNotification(client, "textDocument/didChange", {
|
|
663
658
|
textDocument: { uri, version },
|
|
664
659
|
contentChanges: [{ text: content }],
|
|
665
660
|
});
|
|
661
|
+
throwIfAborted(signal);
|
|
666
662
|
|
|
667
663
|
await sendNotification(client, "textDocument/didSave", {
|
|
668
664
|
textDocument: { uri },
|
|
@@ -745,6 +741,7 @@ export async function sendRequest(
|
|
|
745
741
|
if (client.pendingRequests.has(id)) {
|
|
746
742
|
client.pendingRequests.delete(id);
|
|
747
743
|
}
|
|
744
|
+
void sendNotification(client, "$/cancelRequest", { id }).catch(() => {});
|
|
748
745
|
if (timeout) clearTimeout(timeout);
|
|
749
746
|
cleanup();
|
|
750
747
|
const reason = signal?.reason instanceof Error ? signal.reason : new ToolAbortError();
|
package/src/lsp/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { type Theme, theme } from "../modes/theme/theme";
|
|
|
8
8
|
import lspDescription from "../prompts/tools/lsp.md" with { type: "text" };
|
|
9
9
|
import type { ToolSession } from "../tools";
|
|
10
10
|
import { resolveToCwd } from "../tools/path-utils";
|
|
11
|
-
import { throwIfAborted } from "../tools/tool-errors";
|
|
11
|
+
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
12
12
|
import {
|
|
13
13
|
ensureFileOpen,
|
|
14
14
|
getActiveClients,
|
|
@@ -308,41 +308,52 @@ function detectProjectType(cwd: string): ProjectType {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/** Run workspace diagnostics command and parse output */
|
|
311
|
-
async function runWorkspaceDiagnostics(
|
|
311
|
+
async function runWorkspaceDiagnostics(
|
|
312
|
+
cwd: string,
|
|
313
|
+
signal?: AbortSignal,
|
|
314
|
+
): Promise<{ output: string; projectType: ProjectType }> {
|
|
315
|
+
throwIfAborted(signal);
|
|
312
316
|
const projectType = detectProjectType(cwd);
|
|
313
|
-
|
|
314
317
|
if (!projectType.command) {
|
|
315
318
|
return {
|
|
316
319
|
output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)`,
|
|
317
320
|
projectType,
|
|
318
321
|
};
|
|
319
322
|
}
|
|
323
|
+
const proc = Bun.spawn(projectType.command, {
|
|
324
|
+
cwd,
|
|
325
|
+
stdout: "pipe",
|
|
326
|
+
stderr: "pipe",
|
|
327
|
+
windowsHide: true,
|
|
328
|
+
});
|
|
329
|
+
const abortHandler = () => {
|
|
330
|
+
proc.kill();
|
|
331
|
+
};
|
|
332
|
+
if (signal) {
|
|
333
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
334
|
+
}
|
|
320
335
|
|
|
321
336
|
try {
|
|
322
|
-
const proc = Bun.spawn(projectType.command, {
|
|
323
|
-
cwd,
|
|
324
|
-
stdout: "pipe",
|
|
325
|
-
stderr: "pipe",
|
|
326
|
-
windowsHide: true,
|
|
327
|
-
});
|
|
328
|
-
|
|
329
337
|
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
330
338
|
await proc.exited;
|
|
331
|
-
|
|
339
|
+
throwIfAborted(signal);
|
|
332
340
|
const combined = (stdout + stderr).trim();
|
|
333
341
|
if (!combined) {
|
|
334
342
|
return { output: "No issues found", projectType };
|
|
335
343
|
}
|
|
336
|
-
|
|
337
344
|
// Limit output length
|
|
338
345
|
const lines = combined.split("\n");
|
|
339
346
|
if (lines.length > 50) {
|
|
340
347
|
return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
|
|
341
348
|
}
|
|
342
|
-
|
|
343
349
|
return { output: combined, projectType };
|
|
344
350
|
} catch (e) {
|
|
351
|
+
if (signal?.aborted) {
|
|
352
|
+
throw new ToolAbortError();
|
|
353
|
+
}
|
|
345
354
|
return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
|
|
355
|
+
} finally {
|
|
356
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
346
357
|
}
|
|
347
358
|
}
|
|
348
359
|
|
|
@@ -871,11 +882,12 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
871
882
|
async execute(
|
|
872
883
|
_toolCallId: string,
|
|
873
884
|
params: LspParams,
|
|
874
|
-
|
|
885
|
+
signal?: AbortSignal,
|
|
875
886
|
_onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
|
|
876
887
|
_context?: AgentToolContext,
|
|
877
888
|
): Promise<AgentToolResult<LspToolDetails>> {
|
|
878
889
|
const { action, file, files, line, column, query, new_name, apply, include_declaration } = params;
|
|
890
|
+
throwIfAborted(signal);
|
|
879
891
|
|
|
880
892
|
const config = getConfig(this.session.cwd);
|
|
881
893
|
|
|
@@ -906,7 +918,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
906
918
|
const targets = files?.length ? files : file ? [file] : null;
|
|
907
919
|
if (!targets) {
|
|
908
920
|
// No file specified - run workspace diagnostics
|
|
909
|
-
const result = await runWorkspaceDiagnostics(this.session.cwd);
|
|
921
|
+
const result = await runWorkspaceDiagnostics(this.session.cwd, signal);
|
|
910
922
|
return {
|
|
911
923
|
content: [
|
|
912
924
|
{
|
|
@@ -923,6 +935,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
923
935
|
const allServerNames = new Set<string>();
|
|
924
936
|
|
|
925
937
|
for (const target of targets) {
|
|
938
|
+
throwIfAborted(signal);
|
|
926
939
|
const resolved = resolveToCwd(target, this.session.cwd);
|
|
927
940
|
const servers = getServersForFile(config, resolved);
|
|
928
941
|
if (servers.length === 0) {
|
|
@@ -938,6 +951,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
938
951
|
for (const [serverName, serverConfig] of servers) {
|
|
939
952
|
allServerNames.add(serverName);
|
|
940
953
|
try {
|
|
954
|
+
throwIfAborted(signal);
|
|
941
955
|
if (serverConfig.createClient) {
|
|
942
956
|
const linterClient = getLinterClient(serverName, serverConfig, this.session.cwd);
|
|
943
957
|
const diagnostics = await linterClient.lint(resolved);
|
|
@@ -946,10 +960,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
946
960
|
}
|
|
947
961
|
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
948
962
|
const minVersion = client.diagnosticsVersion;
|
|
949
|
-
await refreshFile(client, resolved);
|
|
950
|
-
const diagnostics = await waitForDiagnostics(client, uri, 3000,
|
|
963
|
+
await refreshFile(client, resolved, signal);
|
|
964
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, signal, minVersion);
|
|
951
965
|
allDiagnostics.push(...diagnostics);
|
|
952
|
-
} catch {
|
|
966
|
+
} catch (err) {
|
|
967
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
968
|
+
throw err;
|
|
969
|
+
}
|
|
953
970
|
// Server failed, continue with others
|
|
954
971
|
}
|
|
955
972
|
}
|
|
@@ -1029,7 +1046,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1029
1046
|
const targetFile = resolvedFile;
|
|
1030
1047
|
|
|
1031
1048
|
if (targetFile) {
|
|
1032
|
-
await ensureFileOpen(client, targetFile);
|
|
1049
|
+
await ensureFileOpen(client, targetFile, signal);
|
|
1033
1050
|
}
|
|
1034
1051
|
|
|
1035
1052
|
const uri = targetFile ? fileToUri(targetFile) : "";
|
|
@@ -1043,10 +1060,15 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1043
1060
|
// =====================================================================
|
|
1044
1061
|
|
|
1045
1062
|
case "definition": {
|
|
1046
|
-
const result = (await sendRequest(
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1063
|
+
const result = (await sendRequest(
|
|
1064
|
+
client,
|
|
1065
|
+
"textDocument/definition",
|
|
1066
|
+
{
|
|
1067
|
+
textDocument: { uri },
|
|
1068
|
+
position,
|
|
1069
|
+
},
|
|
1070
|
+
signal,
|
|
1071
|
+
)) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
1050
1072
|
|
|
1051
1073
|
if (!result) {
|
|
1052
1074
|
output = "No definition found";
|
|
@@ -1076,11 +1098,16 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1076
1098
|
}
|
|
1077
1099
|
|
|
1078
1100
|
case "references": {
|
|
1079
|
-
const result = (await sendRequest(
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1101
|
+
const result = (await sendRequest(
|
|
1102
|
+
client,
|
|
1103
|
+
"textDocument/references",
|
|
1104
|
+
{
|
|
1105
|
+
textDocument: { uri },
|
|
1106
|
+
position,
|
|
1107
|
+
context: { includeDeclaration: include_declaration ?? true },
|
|
1108
|
+
},
|
|
1109
|
+
signal,
|
|
1110
|
+
)) as Location[] | null;
|
|
1084
1111
|
|
|
1085
1112
|
if (!result || result.length === 0) {
|
|
1086
1113
|
output = "No references found";
|
|
@@ -1092,10 +1119,15 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1092
1119
|
}
|
|
1093
1120
|
|
|
1094
1121
|
case "hover": {
|
|
1095
|
-
const result = (await sendRequest(
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1122
|
+
const result = (await sendRequest(
|
|
1123
|
+
client,
|
|
1124
|
+
"textDocument/hover",
|
|
1125
|
+
{
|
|
1126
|
+
textDocument: { uri },
|
|
1127
|
+
position,
|
|
1128
|
+
},
|
|
1129
|
+
signal,
|
|
1130
|
+
)) as Hover | null;
|
|
1099
1131
|
|
|
1100
1132
|
if (!result || !result.contents) {
|
|
1101
1133
|
output = "No hover information";
|
|
@@ -1116,7 +1148,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1116
1148
|
details: { action, serverName, success: false },
|
|
1117
1149
|
};
|
|
1118
1150
|
}
|
|
1119
|
-
const result = (await sendRequest(client, "workspace/symbol", { query })) as
|
|
1151
|
+
const result = (await sendRequest(client, "workspace/symbol", { query }, signal)) as
|
|
1120
1152
|
| SymbolInformation[]
|
|
1121
1153
|
| null;
|
|
1122
1154
|
if (!result || result.length === 0) {
|
|
@@ -1127,9 +1159,14 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1127
1159
|
}
|
|
1128
1160
|
} else {
|
|
1129
1161
|
// File-based document symbols
|
|
1130
|
-
const result = (await sendRequest(
|
|
1131
|
-
|
|
1132
|
-
|
|
1162
|
+
const result = (await sendRequest(
|
|
1163
|
+
client,
|
|
1164
|
+
"textDocument/documentSymbol",
|
|
1165
|
+
{
|
|
1166
|
+
textDocument: { uri },
|
|
1167
|
+
},
|
|
1168
|
+
signal,
|
|
1169
|
+
)) as (DocumentSymbol | SymbolInformation)[] | null;
|
|
1133
1170
|
|
|
1134
1171
|
if (!result || result.length === 0) {
|
|
1135
1172
|
output = "No symbols found";
|
|
@@ -1159,11 +1196,16 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1159
1196
|
};
|
|
1160
1197
|
}
|
|
1161
1198
|
|
|
1162
|
-
const result = (await sendRequest(
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1199
|
+
const result = (await sendRequest(
|
|
1200
|
+
client,
|
|
1201
|
+
"textDocument/rename",
|
|
1202
|
+
{
|
|
1203
|
+
textDocument: { uri },
|
|
1204
|
+
position,
|
|
1205
|
+
newName: new_name,
|
|
1206
|
+
},
|
|
1207
|
+
signal,
|
|
1208
|
+
)) as WorkspaceEdit | null;
|
|
1167
1209
|
|
|
1168
1210
|
if (!result) {
|
|
1169
1211
|
output = "Rename returned no edits";
|
|
@@ -1186,7 +1228,12 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1186
1228
|
const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
|
|
1187
1229
|
for (const method of reloadMethods) {
|
|
1188
1230
|
try {
|
|
1189
|
-
await sendRequest(
|
|
1231
|
+
await sendRequest(
|
|
1232
|
+
client,
|
|
1233
|
+
method,
|
|
1234
|
+
method.includes("Configuration") ? { settings: {} } : null,
|
|
1235
|
+
signal,
|
|
1236
|
+
);
|
|
1190
1237
|
output = `Reloaded ${serverName}`;
|
|
1191
1238
|
break;
|
|
1192
1239
|
} catch {
|
|
@@ -1208,6 +1255,9 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1208
1255
|
details: { serverName, action, success: true, request: params },
|
|
1209
1256
|
};
|
|
1210
1257
|
} catch (err) {
|
|
1258
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1259
|
+
throw new ToolAbortError();
|
|
1260
|
+
}
|
|
1211
1261
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1212
1262
|
return {
|
|
1213
1263
|
content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
|
package/src/mcp/config-writer.ts
CHANGED
|
@@ -180,3 +180,36 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
|
|
|
180
180
|
const config = await readMCPConfigFile(filePath);
|
|
181
181
|
return Object.keys(config.mcpServers ?? {});
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Read the disabled servers list from a config file.
|
|
186
|
+
*/
|
|
187
|
+
export async function readDisabledServers(filePath: string): Promise<string[]> {
|
|
188
|
+
const config = await readMCPConfigFile(filePath);
|
|
189
|
+
return Array.isArray(config.disabledServers) ? config.disabledServers : [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Add or remove a server name from the disabled servers list.
|
|
194
|
+
*/
|
|
195
|
+
export async function setServerDisabled(filePath: string, name: string, disabled: boolean): Promise<void> {
|
|
196
|
+
const config = await readMCPConfigFile(filePath);
|
|
197
|
+
const current = new Set(config.disabledServers ?? []);
|
|
198
|
+
|
|
199
|
+
if (disabled) {
|
|
200
|
+
current.add(name);
|
|
201
|
+
} else {
|
|
202
|
+
current.delete(name);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const updated: MCPConfigFile = {
|
|
206
|
+
...config,
|
|
207
|
+
disabledServers: current.size > 0 ? Array.from(current).sort() : undefined,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (!updated.disabledServers) {
|
|
211
|
+
delete updated.disabledServers;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await writeMCPConfigFile(filePath, updated);
|
|
215
|
+
}
|
package/src/mcp/config.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses the capability system to load MCP servers from multiple sources.
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs";
|
|
6
8
|
import { mcpCapability } from "../capability/mcp";
|
|
7
9
|
import type { SourceMeta } from "../capability/types";
|
|
8
10
|
import type { MCPServer } from "../discovery";
|
|
9
11
|
import { loadCapability } from "../discovery";
|
|
12
|
+
import { readDisabledServers } from "./config-writer";
|
|
10
13
|
import type { MCPServerConfig } from "./types";
|
|
11
14
|
|
|
12
15
|
/** Options for loading MCP configs */
|
|
@@ -97,12 +100,14 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
|
|
|
97
100
|
? result.items
|
|
98
101
|
: result.items.filter(server => server._source.level !== "project");
|
|
99
102
|
|
|
103
|
+
// Load user-level disabled servers list
|
|
104
|
+
const disabledServers = new Set(await readDisabledServers(getMCPConfigPath("user", cwd)));
|
|
100
105
|
// Convert to legacy format and preserve source metadata
|
|
101
106
|
const configs: Record<string, MCPServerConfig> = {};
|
|
102
107
|
const sources: Record<string, SourceMeta> = {};
|
|
103
108
|
for (const server of servers) {
|
|
104
109
|
const config = convertToLegacyConfig(server);
|
|
105
|
-
if (config.enabled === false) {
|
|
110
|
+
if (config.enabled === false || (server._source.level !== "user" && disabledServers.has(server.name))) {
|
|
106
111
|
continue;
|
|
107
112
|
}
|
|
108
113
|
configs[server.name] = config;
|
package/src/mcp/types.ts
CHANGED
|
@@ -89,6 +89,7 @@ export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSs
|
|
|
89
89
|
/** Root .mcp.json file structure */
|
|
90
90
|
export interface MCPConfigFile {
|
|
91
91
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
92
|
+
disabledServers?: string[];
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// =============================================================================
|
|
@@ -5,6 +5,7 @@ import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi
|
|
|
5
5
|
*/
|
|
6
6
|
export class CustomEditor extends Editor {
|
|
7
7
|
onEscape?: () => void;
|
|
8
|
+
shouldBypassAutocompleteOnEscape?: () => boolean;
|
|
8
9
|
onCtrlC?: () => void;
|
|
9
10
|
onCtrlD?: () => void;
|
|
10
11
|
onShiftTab?: () => void;
|
|
@@ -124,11 +125,13 @@ export class CustomEditor extends Editor {
|
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
// Intercept Escape key
|
|
128
|
-
//
|
|
129
|
-
if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape
|
|
130
|
-
this.
|
|
131
|
-
|
|
128
|
+
// Intercept Escape key.
|
|
129
|
+
// Default behavior keeps autocomplete dismissal, but parent can prioritize global escape handling.
|
|
130
|
+
if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape) {
|
|
131
|
+
if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
|
|
132
|
+
this.onEscape();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
// Intercept Ctrl+C
|
|
@@ -149,9 +149,10 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
149
149
|
{
|
|
150
150
|
value: "auto",
|
|
151
151
|
label: "Auto",
|
|
152
|
-
description: "Priority: Exa > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI",
|
|
152
|
+
description: "Priority: Exa > Brave > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI",
|
|
153
153
|
},
|
|
154
154
|
{ value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
|
|
155
|
+
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
155
156
|
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
156
157
|
{ value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
|
|
157
158
|
{ value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
|
|
@@ -334,23 +334,29 @@ export class CommandController {
|
|
|
334
334
|
this.ctx.ui.requestRender();
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
async handleChangelogCommand(): Promise<void> {
|
|
337
|
+
async handleChangelogCommand(showFull = false): Promise<void> {
|
|
338
338
|
const changelogPath = getChangelogPath();
|
|
339
339
|
const allEntries = await parseChangelog(changelogPath);
|
|
340
|
-
|
|
340
|
+
// Default to showing only the latest 3 versions unless --full is specified
|
|
341
|
+
// allEntries comes from parseChangelog with newest first, reverse to show oldest->newest
|
|
342
|
+
const entriesToShow = showFull ? allEntries : allEntries.slice(0, 3);
|
|
341
343
|
const changelogMarkdown =
|
|
342
|
-
|
|
343
|
-
?
|
|
344
|
+
entriesToShow.length > 0
|
|
345
|
+
? [...entriesToShow]
|
|
344
346
|
.reverse()
|
|
345
347
|
.map(e => e.content)
|
|
346
348
|
.join("\n\n")
|
|
347
349
|
: "No changelog entries found.";
|
|
350
|
+
const title = showFull ? "Full Changelog" : "Recent Changes";
|
|
351
|
+
const hint = showFull
|
|
352
|
+
? ""
|
|
353
|
+
: `\n\n${theme.fg("dim", "Use")} ${theme.bold("/changelog full")} ${theme.fg("dim", "to view the complete changelog.")}`;
|
|
348
354
|
|
|
349
355
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
350
356
|
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
351
|
-
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent",
|
|
357
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", title)), 1, 0));
|
|
352
358
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
353
|
-
this.ctx.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
|
359
|
+
this.ctx.chatContainer.addChild(new Markdown(changelogMarkdown + hint, 1, 1, getMarkdownTheme()));
|
|
354
360
|
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
355
361
|
this.ctx.ui.requestRender();
|
|
356
362
|
}
|