@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337

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 (153) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +13 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +189 -29
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +103 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +127 -52
  54. package/src/core/session-manager.ts +123 -20
  55. package/src/core/settings-manager.ts +106 -22
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +94 -0
  60. package/src/core/tools/bash.ts +33 -157
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +5 -5
  63. package/src/core/tools/edit.ts +60 -9
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +6 -5
  75. package/src/core/tools/index.ts +114 -40
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +204 -108
  78. package/src/core/tools/lsp/config.ts +709 -35
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +432 -30
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/types.ts +5 -0
  84. package/src/core/tools/lsp/utils.ts +1 -1
  85. package/src/core/tools/notebook.ts +1 -1
  86. package/src/core/tools/output.ts +175 -0
  87. package/src/core/tools/read.ts +7 -7
  88. package/src/core/tools/renderers.ts +92 -13
  89. package/src/core/tools/review.ts +268 -0
  90. package/src/core/tools/task/agents.ts +1 -1
  91. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  92. package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
  93. package/src/core/tools/task/discovery.ts +2 -2
  94. package/src/core/tools/task/executor.ts +145 -28
  95. package/src/core/tools/task/index.ts +78 -30
  96. package/src/core/tools/task/model-resolver.ts +72 -13
  97. package/src/core/tools/task/parallel.ts +1 -1
  98. package/src/core/tools/task/render.ts +219 -30
  99. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  100. package/src/core/tools/task/types.ts +36 -2
  101. package/src/core/tools/web-fetch.ts +5 -3
  102. package/src/core/tools/web-search/auth.ts +1 -1
  103. package/src/core/tools/web-search/index.ts +17 -15
  104. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  105. package/src/core/tools/web-search/providers/exa.ts +3 -5
  106. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  107. package/src/core/tools/web-search/render.ts +3 -3
  108. package/src/core/tools/write.ts +70 -7
  109. package/src/index.ts +33 -17
  110. package/src/main.ts +60 -34
  111. package/src/migrations.ts +3 -3
  112. package/src/modes/index.ts +5 -5
  113. package/src/modes/interactive/components/armin.ts +1 -1
  114. package/src/modes/interactive/components/assistant-message.ts +1 -1
  115. package/src/modes/interactive/components/bash-execution.ts +4 -4
  116. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  117. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  118. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  119. package/src/modes/interactive/components/diff.ts +1 -1
  120. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  121. package/src/modes/interactive/components/footer.ts +5 -5
  122. package/src/modes/interactive/components/hook-editor.ts +2 -2
  123. package/src/modes/interactive/components/hook-input.ts +2 -2
  124. package/src/modes/interactive/components/hook-message.ts +3 -3
  125. package/src/modes/interactive/components/hook-selector.ts +2 -2
  126. package/src/modes/interactive/components/model-selector.ts +341 -41
  127. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  128. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  129. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  130. package/src/modes/interactive/components/session-selector.ts +24 -11
  131. package/src/modes/interactive/components/settings-defs.ts +51 -3
  132. package/src/modes/interactive/components/settings-selector.ts +13 -16
  133. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  134. package/src/modes/interactive/components/theme-selector.ts +2 -2
  135. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  136. package/src/modes/interactive/components/tool-execution.ts +44 -8
  137. package/src/modes/interactive/components/tree-selector.ts +5 -5
  138. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  139. package/src/modes/interactive/components/user-message.ts +1 -1
  140. package/src/modes/interactive/components/welcome.ts +42 -5
  141. package/src/modes/interactive/interactive-mode.ts +169 -48
  142. package/src/modes/interactive/theme/theme.ts +8 -7
  143. package/src/modes/print-mode.ts +4 -3
  144. package/src/modes/rpc/rpc-client.ts +4 -4
  145. package/src/modes/rpc/rpc-mode.ts +21 -11
  146. package/src/modes/rpc/rpc-types.ts +3 -3
  147. package/src/utils/changelog.ts +2 -2
  148. package/src/utils/clipboard.ts +1 -1
  149. package/src/utils/shell-snapshot.ts +218 -0
  150. package/src/utils/shell.ts +93 -13
  151. package/src/utils/tools-manager.ts +1 -1
  152. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  153. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import { applyWorkspaceEdit } from "./edits.js";
2
+ import { applyWorkspaceEdit } from "./edits";
3
3
  import type {
4
4
  Diagnostic,
5
5
  LspClient,
@@ -8,29 +8,43 @@ import type {
8
8
  LspJsonRpcResponse,
9
9
  ServerConfig,
10
10
  WorkspaceEdit,
11
- } from "./types.js";
12
- import { detectLanguageId, fileToUri } from "./utils.js";
11
+ } from "./types";
12
+ import { detectLanguageId, fileToUri } from "./utils";
13
13
 
14
14
  // =============================================================================
15
15
  // Client State
16
16
  // =============================================================================
17
17
 
18
18
  const clients = new Map<string, LspClient>();
19
+ const clientLocks = new Map<string, Promise<LspClient>>();
20
+ const fileOperationLocks = new Map<string, Promise<void>>();
19
21
 
20
- // Idle timeout: shutdown clients after 5 minutes of inactivity
21
- const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
22
+ // Idle timeout configuration (disabled by default)
23
+ let idleTimeoutMs: number | null = null;
24
+ let idleCheckInterval: Timer | null = null;
22
25
  const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
23
26
 
24
- // Background task to shutdown idle clients
25
- let idleCheckInterval: Timer | null = null;
27
+ /**
28
+ * Configure the idle timeout for LSP clients.
29
+ * @param ms - Timeout in milliseconds, or null/undefined to disable
30
+ */
31
+ export function setIdleTimeout(ms: number | null | undefined): void {
32
+ idleTimeoutMs = ms ?? null;
33
+
34
+ if (idleTimeoutMs && idleTimeoutMs > 0) {
35
+ startIdleChecker();
36
+ } else {
37
+ stopIdleChecker();
38
+ }
39
+ }
26
40
 
27
41
  function startIdleChecker(): void {
28
42
  if (idleCheckInterval) return;
29
43
  idleCheckInterval = setInterval(() => {
44
+ if (!idleTimeoutMs) return;
30
45
  const now = Date.now();
31
46
  for (const [key, client] of Array.from(clients.entries())) {
32
- if (now - client.lastActivity > IDLE_TIMEOUT_MS) {
33
- console.log(`[LSP] Shutting down idle client: ${key}`);
47
+ if (now - client.lastActivity > idleTimeoutMs) {
34
48
  shutdownClient(key);
35
49
  }
36
50
  }
@@ -98,6 +112,12 @@ const CLIENT_CAPABILITIES = {
98
112
  properties: ["edit"],
99
113
  },
100
114
  },
115
+ formatting: {
116
+ dynamicRegistration: false,
117
+ },
118
+ rangeFormatting: {
119
+ dynamicRegistration: false,
120
+ },
101
121
  publishDiagnostics: {
102
122
  relatedInformation: true,
103
123
  versionSupport: false,
@@ -215,13 +235,17 @@ async function startMessageReader(client: LspClient): Promise<void> {
215
235
  const { done, value } = await reader.read();
216
236
  if (done) break;
217
237
 
218
- client.messageBuffer = concatBuffers(client.messageBuffer, value);
238
+ // Atomically update buffer before processing
239
+ const currentBuffer = concatBuffers(client.messageBuffer, value);
240
+ client.messageBuffer = currentBuffer;
219
241
 
220
242
  // Process all complete messages in buffer
221
- let parsed = parseMessage(client.messageBuffer);
243
+ // Use local variable to avoid race with concurrent buffer updates
244
+ let workingBuffer = currentBuffer;
245
+ let parsed = parseMessage(workingBuffer);
222
246
  while (parsed) {
223
247
  const { message, remaining } = parsed;
224
- client.messageBuffer = remaining;
248
+ workingBuffer = remaining;
225
249
 
226
250
  // Route message
227
251
  if ("id" in message && message.id !== undefined) {
@@ -245,8 +269,11 @@ async function startMessageReader(client: LspClient): Promise<void> {
245
269
  }
246
270
  }
247
271
 
248
- parsed = parseMessage(client.messageBuffer);
272
+ parsed = parseMessage(workingBuffer);
249
273
  }
274
+
275
+ // Atomically commit processed buffer
276
+ client.messageBuffer = workingBuffer;
250
277
  }
251
278
  } catch (err) {
252
279
  // Connection closed or error - reject all pending requests
@@ -350,77 +377,88 @@ async function sendResponse(
350
377
  export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
351
378
  const key = `${config.command}:${cwd}`;
352
379
 
353
- if (clients.has(key)) {
354
- const client = clients.get(key)!;
355
- client.lastActivity = Date.now();
356
- return client;
380
+ // Check if client already exists
381
+ const existingClient = clients.get(key);
382
+ if (existingClient) {
383
+ existingClient.lastActivity = Date.now();
384
+ return existingClient;
357
385
  }
358
386
 
359
- const args = config.args ?? [];
360
- const proc = Bun.spawn([config.command, ...args], {
361
- cwd,
362
- stdin: "pipe",
363
- stdout: "pipe",
364
- stderr: "pipe",
365
- });
366
-
367
- const client: LspClient = {
368
- name: key,
369
- cwd,
370
- process: proc,
371
- config,
372
- requestId: 0,
373
- diagnostics: new Map(),
374
- openFiles: new Map(),
375
- pendingRequests: new Map(),
376
- messageBuffer: new Uint8Array(0),
377
- isReading: false,
378
- lastActivity: Date.now(),
379
- };
380
- clients.set(key, client);
387
+ // Check if another coroutine is already creating this client
388
+ const existingLock = clientLocks.get(key);
389
+ if (existingLock) {
390
+ return existingLock;
391
+ }
381
392
 
382
- // Start idle checker if not already running
383
- startIdleChecker();
393
+ // Create new client with lock
394
+ const clientPromise = (async () => {
395
+ const args = config.args ?? [];
396
+ const command = config.resolvedCommand ?? config.command;
397
+ const proc = Bun.spawn([command, ...args], {
398
+ cwd,
399
+ stdin: "pipe",
400
+ stdout: "pipe",
401
+ stderr: "pipe",
402
+ });
384
403
 
385
- // Register crash recovery - remove client on process exit
386
- proc.exited.then(() => {
387
- console.log(`[LSP] Process exited: ${key}`);
388
- clients.delete(key);
389
- if (clients.size === 0) {
390
- stopIdleChecker();
391
- }
392
- });
404
+ const client: LspClient = {
405
+ name: key,
406
+ cwd,
407
+ process: proc,
408
+ config,
409
+ requestId: 0,
410
+ diagnostics: new Map(),
411
+ openFiles: new Map(),
412
+ pendingRequests: new Map(),
413
+ messageBuffer: new Uint8Array(0),
414
+ isReading: false,
415
+ lastActivity: Date.now(),
416
+ };
417
+ clients.set(key, client);
418
+
419
+ // Register crash recovery - remove client on process exit
420
+ proc.exited.then(() => {
421
+ clients.delete(key);
422
+ clientLocks.delete(key);
423
+ });
393
424
 
394
- // Start background message reader
395
- startMessageReader(client);
425
+ // Start background message reader
426
+ startMessageReader(client);
427
+
428
+ try {
429
+ // Send initialize request
430
+ const initResult = (await sendRequest(client, "initialize", {
431
+ processId: process.pid,
432
+ rootUri: fileToUri(cwd),
433
+ rootPath: cwd,
434
+ capabilities: CLIENT_CAPABILITIES,
435
+ initializationOptions: config.initOptions ?? {},
436
+ workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
437
+ })) as { capabilities?: unknown };
438
+
439
+ if (!initResult) {
440
+ throw new Error("Failed to initialize LSP: no response");
441
+ }
396
442
 
397
- try {
398
- // Send initialize request
399
- const initResult = (await sendRequest(client, "initialize", {
400
- processId: process.pid,
401
- rootUri: fileToUri(cwd),
402
- rootPath: cwd,
403
- capabilities: CLIENT_CAPABILITIES,
404
- initializationOptions: config.initOptions ?? {},
405
- workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
406
- })) as { capabilities?: unknown };
407
-
408
- if (!initResult) {
409
- throw new Error("Failed to initialize LSP: no response");
410
- }
443
+ client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
411
444
 
412
- client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
445
+ // Send initialized notification
446
+ await sendNotification(client, "initialized", {});
413
447
 
414
- // Send initialized notification
415
- await sendNotification(client, "initialized", {});
448
+ return client;
449
+ } catch (err) {
450
+ // Clean up on initialization failure
451
+ clients.delete(key);
452
+ clientLocks.delete(key);
453
+ proc.kill();
454
+ throw err;
455
+ } finally {
456
+ clientLocks.delete(key);
457
+ }
458
+ })();
416
459
 
417
- return client;
418
- } catch (err) {
419
- // Clean up on initialization failure
420
- clients.delete(key);
421
- proc.kill();
422
- throw err;
423
- }
460
+ clientLocks.set(key, clientPromise);
461
+ return clientPromise;
424
462
  }
425
463
 
426
464
  /**
@@ -429,24 +467,49 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
429
467
  */
430
468
  export async function ensureFileOpen(client: LspClient, filePath: string): Promise<void> {
431
469
  const uri = fileToUri(filePath);
470
+ const lockKey = `${client.name}:${uri}`;
471
+
472
+ // Check if file is already open
432
473
  if (client.openFiles.has(uri)) {
433
474
  return;
434
475
  }
435
476
 
436
- const content = fs.readFileSync(filePath, "utf-8");
437
- const languageId = detectLanguageId(filePath);
477
+ // Check if another operation is already opening this file
478
+ const existingLock = fileOperationLocks.get(lockKey);
479
+ if (existingLock) {
480
+ await existingLock;
481
+ return;
482
+ }
438
483
 
439
- await sendNotification(client, "textDocument/didOpen", {
440
- textDocument: {
441
- uri,
442
- languageId,
443
- version: 1,
444
- text: content,
445
- },
446
- });
484
+ // Lock and open file
485
+ const openPromise = (async () => {
486
+ // Double-check after acquiring lock
487
+ if (client.openFiles.has(uri)) {
488
+ return;
489
+ }
447
490
 
448
- client.openFiles.set(uri, { version: 1, languageId });
449
- client.lastActivity = Date.now();
491
+ const content = fs.readFileSync(filePath, "utf-8");
492
+ const languageId = detectLanguageId(filePath);
493
+
494
+ await sendNotification(client, "textDocument/didOpen", {
495
+ textDocument: {
496
+ uri,
497
+ languageId,
498
+ version: 1,
499
+ text: content,
500
+ },
501
+ });
502
+
503
+ client.openFiles.set(uri, { version: 1, languageId });
504
+ client.lastActivity = Date.now();
505
+ })();
506
+
507
+ fileOperationLocks.set(lockKey, openPromise);
508
+ try {
509
+ await openPromise;
510
+ } finally {
511
+ fileOperationLocks.delete(lockKey);
512
+ }
450
513
  }
451
514
 
452
515
  /**
@@ -455,27 +518,45 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
455
518
  */
456
519
  export async function refreshFile(client: LspClient, filePath: string): Promise<void> {
457
520
  const uri = fileToUri(filePath);
458
- const info = client.openFiles.get(uri);
521
+ const lockKey = `${client.name}:${uri}`;
459
522
 
460
- if (!info) {
461
- await ensureFileOpen(client, filePath);
462
- return;
523
+ // Check if another operation is in progress
524
+ const existingLock = fileOperationLocks.get(lockKey);
525
+ if (existingLock) {
526
+ await existingLock;
463
527
  }
464
528
 
465
- const content = fs.readFileSync(filePath, "utf-8");
466
- info.version++;
529
+ // Lock and refresh file
530
+ const refreshPromise = (async () => {
531
+ const info = client.openFiles.get(uri);
467
532
 
468
- await sendNotification(client, "textDocument/didChange", {
469
- textDocument: { uri, version: info.version },
470
- contentChanges: [{ text: content }],
471
- });
533
+ if (!info) {
534
+ await ensureFileOpen(client, filePath);
535
+ return;
536
+ }
472
537
 
473
- await sendNotification(client, "textDocument/didSave", {
474
- textDocument: { uri },
475
- text: content,
476
- });
538
+ const content = fs.readFileSync(filePath, "utf-8");
539
+ const version = ++info.version;
477
540
 
478
- client.lastActivity = Date.now();
541
+ await sendNotification(client, "textDocument/didChange", {
542
+ textDocument: { uri, version },
543
+ contentChanges: [{ text: content }],
544
+ });
545
+
546
+ await sendNotification(client, "textDocument/didSave", {
547
+ textDocument: { uri },
548
+ text: content,
549
+ });
550
+
551
+ client.lastActivity = Date.now();
552
+ })();
553
+
554
+ fileOperationLocks.set(lockKey, refreshPromise);
555
+ try {
556
+ await refreshPromise;
557
+ } finally {
558
+ fileOperationLocks.delete(lockKey);
559
+ }
479
560
  }
480
561
 
481
562
  /**
@@ -497,10 +578,6 @@ export function shutdownClient(key: string): void {
497
578
  // Kill process
498
579
  client.process.kill();
499
580
  clients.delete(key);
500
-
501
- if (clients.size === 0) {
502
- stopIdleChecker();
503
- }
504
581
  }
505
582
 
506
583
  // =============================================================================
@@ -511,7 +588,9 @@ export function shutdownClient(key: string): void {
511
588
  * Send an LSP request and wait for response.
512
589
  */
513
590
  export async function sendRequest(client: LspClient, method: string, params: unknown): Promise<unknown> {
591
+ // Atomically increment and capture request ID
514
592
  const id = ++client.requestId;
593
+
515
594
  const request: LspJsonRpcRequest = {
516
595
  jsonrpc: "2.0",
517
596
  id,
@@ -570,8 +649,6 @@ export async function sendNotification(client: LspClient, method: string, params
570
649
  * Shutdown all LSP clients.
571
650
  */
572
651
  export function shutdownAll(): void {
573
- stopIdleChecker();
574
-
575
652
  for (const client of Array.from(clients.values())) {
576
653
  // Reject all pending requests
577
654
  for (const pending of Array.from(client.pendingRequests.values())) {
@@ -587,6 +664,25 @@ export function shutdownAll(): void {
587
664
  clients.clear();
588
665
  }
589
666
 
667
+ /** Status of an LSP server */
668
+ export interface LspServerStatus {
669
+ name: string;
670
+ status: "connecting" | "ready" | "error";
671
+ fileTypes: string[];
672
+ error?: string;
673
+ }
674
+
675
+ /**
676
+ * Get status of all active LSP clients.
677
+ */
678
+ export function getActiveClients(): LspServerStatus[] {
679
+ return Array.from(clients.values()).map((client) => ({
680
+ name: client.config.command,
681
+ status: "ready" as const,
682
+ fileTypes: client.config.fileTypes,
683
+ }));
684
+ }
685
+
590
686
  // =============================================================================
591
687
  // Process Cleanup
592
688
  // =============================================================================