@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +37 -37
  2. package/README.md +9 -1052
  3. package/package.json +7 -7
  4. package/src/cli/args.ts +1 -0
  5. package/src/cli/update-cli.ts +49 -35
  6. package/src/cli/web-search-cli.ts +3 -2
  7. package/src/commands/web-search.ts +1 -0
  8. package/src/config/model-registry.ts +6 -0
  9. package/src/config/settings-schema.ts +25 -3
  10. package/src/config/settings.ts +1 -0
  11. package/src/extensibility/extensions/wrapper.ts +20 -13
  12. package/src/extensibility/slash-commands.ts +12 -91
  13. package/src/lsp/client.ts +24 -27
  14. package/src/lsp/index.ts +92 -42
  15. package/src/mcp/config-writer.ts +33 -0
  16. package/src/mcp/config.ts +6 -1
  17. package/src/mcp/types.ts +1 -0
  18. package/src/modes/components/custom-editor.ts +8 -5
  19. package/src/modes/components/settings-defs.ts +2 -1
  20. package/src/modes/controllers/command-controller.ts +12 -6
  21. package/src/modes/controllers/input-controller.ts +21 -186
  22. package/src/modes/controllers/mcp-command-controller.ts +60 -3
  23. package/src/modes/interactive-mode.ts +2 -2
  24. package/src/modes/types.ts +1 -1
  25. package/src/sdk.ts +23 -1
  26. package/src/secrets/index.ts +116 -0
  27. package/src/secrets/obfuscator.ts +269 -0
  28. package/src/secrets/regex.ts +21 -0
  29. package/src/session/agent-session.ts +143 -21
  30. package/src/session/compaction/branch-summarization.ts +2 -2
  31. package/src/session/compaction/compaction.ts +10 -3
  32. package/src/session/compaction/utils.ts +25 -1
  33. package/src/slash-commands/builtin-registry.ts +419 -0
  34. package/src/web/scrapers/github.ts +50 -12
  35. package/src/web/search/index.ts +5 -5
  36. package/src/web/search/provider.ts +13 -2
  37. package/src/web/search/providers/brave.ts +165 -0
  38. package/src/web/search/types.ts +1 -1
  39. package/docs/compaction.md +0 -436
  40. package/docs/config-usage.md +0 -176
  41. package/docs/custom-tools.md +0 -585
  42. package/docs/environment-variables.md +0 -257
  43. package/docs/extension-loading.md +0 -106
  44. package/docs/extensions.md +0 -1342
  45. package/docs/fs-scan-cache-architecture.md +0 -50
  46. package/docs/hooks.md +0 -906
  47. package/docs/models.md +0 -234
  48. package/docs/python-repl.md +0 -110
  49. package/docs/rpc.md +0 -1173
  50. package/docs/sdk.md +0 -1039
  51. package/docs/session-tree-plan.md +0 -84
  52. package/docs/session.md +0 -368
  53. package/docs/skills.md +0 -254
  54. package/docs/theme.md +0 -696
  55. package/docs/tree.md +0 -206
  56. 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: Uint8Array,
161
- ): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining: Uint8Array } | null {
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.slice(messageStart, messageEnd);
176
+ const messageBytes = buffer.subarray(messageStart, messageEnd);
177
177
  const messageText = new TextDecoder().decode(messageBytes);
178
- const remaining = buffer.slice(messageEnd);
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
- const contentBytes = new TextEncoder().encode(content);
214
- const header = `Content-Length: ${contentBytes.length}\r\n\r\n`;
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 = concatBuffers(client.messageBuffer, value);
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(cwd: string): Promise<{ output: string; projectType: ProjectType }> {
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
- _signal?: AbortSignal,
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, undefined, minVersion);
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(client, "textDocument/definition", {
1047
- textDocument: { uri },
1048
- position,
1049
- })) as Location | Location[] | LocationLink | LocationLink[] | null;
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(client, "textDocument/references", {
1080
- textDocument: { uri },
1081
- position,
1082
- context: { includeDeclaration: include_declaration ?? true },
1083
- })) as Location[] | null;
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(client, "textDocument/hover", {
1096
- textDocument: { uri },
1097
- position,
1098
- })) as Hover | null;
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(client, "textDocument/documentSymbol", {
1131
- textDocument: { uri },
1132
- })) as (DocumentSymbol | SymbolInformation)[] | null;
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(client, "textDocument/rename", {
1163
- textDocument: { uri },
1164
- position,
1165
- newName: new_name,
1166
- })) as WorkspaceEdit | null;
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(client, method, method.includes("Configuration") ? { settings: {} } : null);
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}` }],
@@ -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 - but only if autocomplete is NOT active
128
- // (let parent handle escape for autocomplete cancellation)
129
- if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape && !this.isShowingAutocomplete()) {
130
- this.onEscape();
131
- return;
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
- allEntries.length > 0
343
- ? allEntries
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", "What's New")), 1, 0));
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
  }