@oh-my-pi/pi-coding-agent 1.338.0 → 1.341.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 (42) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/package.json +3 -3
  3. package/src/cli/args.ts +8 -0
  4. package/src/core/agent-session.ts +32 -14
  5. package/src/core/export-html/index.ts +48 -15
  6. package/src/core/export-html/template.html +3 -11
  7. package/src/core/mcp/client.ts +43 -16
  8. package/src/core/mcp/config.ts +152 -6
  9. package/src/core/mcp/index.ts +6 -2
  10. package/src/core/mcp/loader.ts +30 -3
  11. package/src/core/mcp/manager.ts +69 -10
  12. package/src/core/mcp/types.ts +9 -3
  13. package/src/core/model-resolver.ts +101 -0
  14. package/src/core/sdk.ts +65 -18
  15. package/src/core/session-manager.ts +117 -14
  16. package/src/core/settings-manager.ts +107 -19
  17. package/src/core/title-generator.ts +94 -0
  18. package/src/core/tools/bash.ts +1 -2
  19. package/src/core/tools/edit-diff.ts +2 -2
  20. package/src/core/tools/edit.ts +43 -5
  21. package/src/core/tools/grep.ts +3 -2
  22. package/src/core/tools/index.ts +73 -13
  23. package/src/core/tools/lsp/client.ts +45 -20
  24. package/src/core/tools/lsp/config.ts +708 -34
  25. package/src/core/tools/lsp/index.ts +423 -23
  26. package/src/core/tools/lsp/types.ts +5 -0
  27. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  28. package/src/core/tools/task/bundled-agents/reviewer.md +1 -1
  29. package/src/core/tools/task/model-resolver.ts +52 -3
  30. package/src/core/tools/write.ts +67 -4
  31. package/src/index.ts +5 -0
  32. package/src/main.ts +23 -2
  33. package/src/modes/interactive/components/model-selector.ts +96 -18
  34. package/src/modes/interactive/components/session-selector.ts +20 -7
  35. package/src/modes/interactive/components/settings-defs.ts +59 -2
  36. package/src/modes/interactive/components/settings-selector.ts +8 -11
  37. package/src/modes/interactive/components/tool-execution.ts +18 -0
  38. package/src/modes/interactive/components/tree-selector.ts +2 -2
  39. package/src/modes/interactive/components/welcome.ts +40 -3
  40. package/src/modes/interactive/interactive-mode.ts +87 -10
  41. package/src/core/export-html/vendor/highlight.min.js +0 -1213
  42. package/src/core/export-html/vendor/marked.min.js +0 -6
@@ -17,24 +17,49 @@ export interface MCPToolsLoadResult {
17
17
  errors: Array<{ path: string; error: string }>;
18
18
  /** Connected server names */
19
19
  connectedServers: string[];
20
+ /** Extracted Exa API keys from filtered MCP servers */
21
+ exaApiKeys: string[];
22
+ }
23
+
24
+ /** Options for loading MCP tools */
25
+ export interface MCPToolsLoadOptions {
26
+ /** Additional environment variables for expansion */
27
+ extraEnv?: Record<string, string>;
28
+ /** Called when starting to connect to servers */
29
+ onConnecting?: (serverNames: string[]) => void;
30
+ /** Whether to load project-level config (default: true) */
31
+ enableProjectConfig?: boolean;
32
+ /** Whether to filter out Exa MCP servers (default: true) */
33
+ filterExa?: boolean;
20
34
  }
21
35
 
22
36
  /**
23
37
  * Discover and load MCP tools from .mcp.json files.
24
38
  *
25
39
  * @param cwd Working directory (project root)
26
- * @param extraEnv Additional environment variables for expansion
40
+ * @param options Load options including extraEnv and progress callbacks
27
41
  * @returns MCP tools in LoadedCustomTool format for integration
28
42
  */
29
43
  export async function discoverAndLoadMCPTools(
30
44
  cwd: string,
31
- extraEnv?: Record<string, string>,
45
+ options?: MCPToolsLoadOptions | Record<string, string>,
32
46
  ): Promise<MCPToolsLoadResult> {
47
+ // Support old signature: discoverAndLoadMCPTools(cwd, extraEnv)
48
+ const opts: MCPToolsLoadOptions =
49
+ options && ("extraEnv" in options || "onConnecting" in options || "enableProjectConfig" in options)
50
+ ? (options as MCPToolsLoadOptions)
51
+ : { extraEnv: options as Record<string, string> | undefined };
52
+
33
53
  const manager = new MCPManager(cwd);
34
54
 
35
55
  let result: MCPLoadResult;
36
56
  try {
37
- result = await manager.discoverAndConnect(extraEnv);
57
+ result = await manager.discoverAndConnect({
58
+ extraEnv: opts.extraEnv,
59
+ onConnecting: opts.onConnecting,
60
+ enableProjectConfig: opts.enableProjectConfig,
61
+ filterExa: opts.filterExa,
62
+ });
38
63
  } catch (error) {
39
64
  // If discovery fails entirely, return empty result
40
65
  const message = error instanceof Error ? error.message : String(error);
@@ -43,6 +68,7 @@ export async function discoverAndLoadMCPTools(
43
68
  tools: [],
44
69
  errors: [{ path: ".mcp.json", error: message }],
45
70
  connectedServers: [],
71
+ exaApiKeys: [],
46
72
  };
47
73
  }
48
74
 
@@ -64,5 +90,6 @@ export async function discoverAndLoadMCPTools(
64
90
  tools: loadedTools,
65
91
  errors,
66
92
  connectedServers: result.connectedServers,
93
+ exaApiKeys: result.exaApiKeys,
67
94
  };
68
95
  }
@@ -8,7 +8,7 @@
8
8
  import type { TSchema } from "@sinclair/typebox";
9
9
  import type { CustomTool } from "../custom-tools/types.js";
10
10
  import { connectToServer, disconnectServer, listTools } from "./client.js";
11
- import { loadAllMCPConfigs, validateServerConfig } from "./config.js";
11
+ import { type LoadMCPConfigsOptions, loadAllMCPConfigs, validateServerConfig } from "./config.js";
12
12
  import type { MCPToolDetails } from "./tool-bridge.js";
13
13
  import { createMCPTools } from "./tool-bridge.js";
14
14
  import type { MCPServerConfig, MCPServerConnection } from "./types.js";
@@ -21,6 +21,14 @@ export interface MCPLoadResult {
21
21
  errors: Map<string, string>;
22
22
  /** Connected server names */
23
23
  connectedServers: string[];
24
+ /** Extracted Exa API keys from filtered MCP servers */
25
+ exaApiKeys: string[];
26
+ }
27
+
28
+ /** Options for discovering and connecting to MCP servers */
29
+ export interface MCPDiscoverOptions extends LoadMCPConfigsOptions {
30
+ /** Called when starting to connect to servers */
31
+ onConnecting?: (serverNames: string[]) => void;
24
32
  }
25
33
 
26
34
  /**
@@ -38,19 +46,49 @@ export class MCPManager {
38
46
  * Discover and connect to all MCP servers from .mcp.json files.
39
47
  * Returns tools and any connection errors.
40
48
  */
41
- async discoverAndConnect(extraEnv?: Record<string, string>): Promise<MCPLoadResult> {
42
- const configs = loadAllMCPConfigs(this.cwd, extraEnv);
43
- return this.connectServers(configs);
49
+ async discoverAndConnect(
50
+ extraEnvOrOptions?: Record<string, string> | MCPDiscoverOptions,
51
+ onConnecting?: (serverNames: string[]) => void,
52
+ ): Promise<MCPLoadResult> {
53
+ // Support old signature: discoverAndConnect(extraEnv, onConnecting)
54
+ const opts: MCPDiscoverOptions =
55
+ extraEnvOrOptions &&
56
+ ("extraEnv" in extraEnvOrOptions ||
57
+ "enableProjectConfig" in extraEnvOrOptions ||
58
+ "filterExa" in extraEnvOrOptions ||
59
+ "onConnecting" in extraEnvOrOptions)
60
+ ? (extraEnvOrOptions as MCPDiscoverOptions)
61
+ : { extraEnv: extraEnvOrOptions as Record<string, string> | undefined, onConnecting };
62
+
63
+ const { configs, exaApiKeys } = loadAllMCPConfigs(this.cwd, {
64
+ extraEnv: opts.extraEnv,
65
+ enableProjectConfig: opts.enableProjectConfig,
66
+ filterExa: opts.filterExa,
67
+ });
68
+ const result = await this.connectServers(configs, opts.onConnecting);
69
+ result.exaApiKeys = exaApiKeys;
70
+ return result;
44
71
  }
45
72
 
46
73
  /**
47
74
  * Connect to specific MCP servers.
75
+ * Connections are made in parallel for faster startup.
48
76
  */
49
- async connectServers(configs: Record<string, MCPServerConfig>): Promise<MCPLoadResult> {
77
+ async connectServers(
78
+ configs: Record<string, MCPServerConfig>,
79
+ onConnecting?: (serverNames: string[]) => void,
80
+ ): Promise<MCPLoadResult> {
50
81
  const errors = new Map<string, string>();
51
82
  const connectedServers: string[] = [];
52
83
  const allTools: CustomTool<TSchema, MCPToolDetails>[] = [];
53
84
 
85
+ // Prepare connection tasks
86
+ const connectionTasks: Array<{
87
+ name: string;
88
+ config: MCPServerConfig;
89
+ validationErrors: string[];
90
+ }> = [];
91
+
54
92
  for (const [name, config] of Object.entries(configs)) {
55
93
  // Skip if already connected
56
94
  if (this.connections.has(name)) {
@@ -65,17 +103,37 @@ export class MCPManager {
65
103
  continue;
66
104
  }
67
105
 
68
- try {
106
+ connectionTasks.push({ name, config, validationErrors });
107
+ }
108
+
109
+ // Notify about servers we're connecting to
110
+ if (connectionTasks.length > 0 && onConnecting) {
111
+ onConnecting(connectionTasks.map((t) => t.name));
112
+ }
113
+
114
+ // Connect to all servers in parallel
115
+ const results = await Promise.allSettled(
116
+ connectionTasks.map(async ({ name, config }) => {
69
117
  const connection = await connectToServer(name, config);
118
+ const serverTools = await listTools(connection);
119
+ return { name, connection, serverTools };
120
+ }),
121
+ );
122
+
123
+ // Process results
124
+ for (let i = 0; i < results.length; i++) {
125
+ const result = results[i];
126
+ const { name } = connectionTasks[i];
127
+
128
+ if (result.status === "fulfilled") {
129
+ const { connection, serverTools } = result.value;
70
130
  this.connections.set(name, connection);
71
131
  connectedServers.push(name);
72
132
 
73
- // Load tools from this server
74
- const serverTools = await listTools(connection);
75
133
  const customTools = createMCPTools(connection, serverTools);
76
134
  allTools.push(...customTools);
77
- } catch (error) {
78
- const message = error instanceof Error ? error.message : String(error);
135
+ } else {
136
+ const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
79
137
  errors.set(name, message);
80
138
  }
81
139
  }
@@ -87,6 +145,7 @@ export class MCPManager {
87
145
  tools: allTools,
88
146
  errors,
89
147
  connectedServers,
148
+ exaApiKeys: [], // Will be populated by discoverAndConnect
90
149
  };
91
150
  }
92
151
 
@@ -41,8 +41,14 @@ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcRespo
41
41
  // MCP Server Configuration (.mcp.json format)
42
42
  // =============================================================================
43
43
 
44
+ /** Base server config with shared options */
45
+ interface MCPServerConfigBase {
46
+ /** Connection timeout in milliseconds (default: 30000) */
47
+ timeout?: number;
48
+ }
49
+
44
50
  /** Stdio server configuration */
45
- export interface MCPStdioServerConfig {
51
+ export interface MCPStdioServerConfig extends MCPServerConfigBase {
46
52
  type?: "stdio"; // Default if not specified
47
53
  command: string;
48
54
  args?: string[];
@@ -51,14 +57,14 @@ export interface MCPStdioServerConfig {
51
57
  }
52
58
 
53
59
  /** HTTP server configuration (Streamable HTTP transport) */
54
- export interface MCPHttpServerConfig {
60
+ export interface MCPHttpServerConfig extends MCPServerConfigBase {
55
61
  type: "http";
56
62
  url: string;
57
63
  headers?: Record<string, string>;
58
64
  }
59
65
 
60
66
  /** SSE server configuration (deprecated, use HTTP) */
61
- export interface MCPSseServerConfig {
67
+ export interface MCPSseServerConfig extends MCPServerConfigBase {
62
68
  type: "sse";
63
69
  url: string;
64
70
  headers?: Record<string, string>;
@@ -30,6 +30,29 @@ export interface ScopedModel {
30
30
  thinkingLevel: ThinkingLevel;
31
31
  }
32
32
 
33
+ /** Priority chain for auto-discovering smol/fast models */
34
+ export const SMOL_MODEL_PRIORITY = ["claude-haiku-4-5", "haiku", "flash", "mini"];
35
+
36
+ /** Priority chain for auto-discovering slow/comprehensive models (reasoning, codex) */
37
+ export const SLOW_MODEL_PRIORITY = ["gpt-5.2-codex", "gpt-5.2", "codex", "gpt", "opus", "pro"];
38
+
39
+ /**
40
+ * Parse a model string in "provider/modelId" format.
41
+ * Returns undefined if the format is invalid.
42
+ */
43
+ export function parseModelString(modelStr: string): { provider: string; id: string } | undefined {
44
+ const slashIdx = modelStr.indexOf("/");
45
+ if (slashIdx <= 0) return undefined;
46
+ return { provider: modelStr.slice(0, slashIdx), id: modelStr.slice(slashIdx + 1) };
47
+ }
48
+
49
+ /**
50
+ * Format a model as "provider/modelId" string.
51
+ */
52
+ export function formatModelString(model: Model<Api>): string {
53
+ return `${model.provider}/${model.id}`;
54
+ }
55
+
33
56
  /**
34
57
  * Helper to check if a model ID looks like an alias (no date suffix)
35
58
  * Dates are typically in format: -20241022 or -20250929
@@ -391,3 +414,81 @@ export async function restoreModelFromSession(
391
414
  // No models available
392
415
  return { model: undefined, fallbackMessage: undefined };
393
416
  }
417
+
418
+ /**
419
+ * Find a smol/fast model using the priority chain.
420
+ * Tries exact matches first, then fuzzy matches.
421
+ *
422
+ * @param modelRegistry The model registry to search
423
+ * @param savedModel Optional saved model string from settings (provider/modelId)
424
+ * @returns The best available smol model, or undefined if none found
425
+ */
426
+ export async function findSmolModel(
427
+ modelRegistry: ModelRegistry,
428
+ savedModel?: string,
429
+ ): Promise<Model<Api> | undefined> {
430
+ const availableModels = await modelRegistry.getAvailable();
431
+ if (availableModels.length === 0) return undefined;
432
+
433
+ // 1. Try saved model from settings
434
+ if (savedModel) {
435
+ const parsed = parseModelString(savedModel);
436
+ if (parsed) {
437
+ const match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
438
+ if (match) return match;
439
+ }
440
+ }
441
+
442
+ // 2. Try priority chain
443
+ for (const pattern of SMOL_MODEL_PRIORITY) {
444
+ // Try exact match first
445
+ const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern.toLowerCase());
446
+ if (exactMatch) return exactMatch;
447
+
448
+ // Try fuzzy match (substring)
449
+ const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern.toLowerCase()));
450
+ if (fuzzyMatch) return fuzzyMatch;
451
+ }
452
+
453
+ // 3. Fallback to first available (same as default)
454
+ return availableModels[0];
455
+ }
456
+
457
+ /**
458
+ * Find a slow/comprehensive model using the priority chain.
459
+ * Prioritizes reasoning and codex models for thorough analysis.
460
+ *
461
+ * @param modelRegistry The model registry to search
462
+ * @param savedModel Optional saved model string from settings (provider/modelId)
463
+ * @returns The best available slow model, or undefined if none found
464
+ */
465
+ export async function findSlowModel(
466
+ modelRegistry: ModelRegistry,
467
+ savedModel?: string,
468
+ ): Promise<Model<Api> | undefined> {
469
+ const availableModels = await modelRegistry.getAvailable();
470
+ if (availableModels.length === 0) return undefined;
471
+
472
+ // 1. Try saved model from settings
473
+ if (savedModel) {
474
+ const parsed = parseModelString(savedModel);
475
+ if (parsed) {
476
+ const match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
477
+ if (match) return match;
478
+ }
479
+ }
480
+
481
+ // 2. Try priority chain
482
+ for (const pattern of SLOW_MODEL_PRIORITY) {
483
+ // Try exact match first
484
+ const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern.toLowerCase());
485
+ if (exactMatch) return exactMatch;
486
+
487
+ // Try fuzzy match (substring)
488
+ const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern.toLowerCase()));
489
+ if (fuzzyMatch) return fuzzyMatch;
490
+ }
491
+
492
+ // 3. Fallback to first available (same as default)
493
+ return availableModels[0];
494
+ }
package/src/core/sdk.ts CHANGED
@@ -78,6 +78,7 @@ import {
78
78
  readOnlyTools,
79
79
  readTool,
80
80
  type Tool,
81
+ warmupLspServers,
81
82
  writeTool,
82
83
  } from "./tools/index.js";
83
84
 
@@ -146,6 +147,8 @@ export interface CreateAgentSessionResult {
146
147
  mcpManager?: MCPManager;
147
148
  /** Warning if session was restored with a different model than saved */
148
149
  modelFallbackMessage?: string;
150
+ /** LSP servers that were warmed up at startup */
151
+ lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>;
149
152
  }
150
153
 
151
154
  // Re-exports
@@ -323,8 +326,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
323
326
  export function loadSettings(cwd?: string, agentDir?: string): Settings {
324
327
  const manager = SettingsManager.create(cwd ?? process.cwd(), agentDir ?? getDefaultAgentDir());
325
328
  return {
326
- defaultProvider: manager.getDefaultProvider(),
327
- defaultModel: manager.getDefaultModel(),
329
+ modelRoles: manager.getModelRoles(),
328
330
  defaultThinkingLevel: manager.getDefaultThinkingLevel(),
329
331
  queueMode: manager.getQueueMode(),
330
332
  theme: manager.getTheme(),
@@ -479,24 +481,34 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
479
481
  let modelFallbackMessage: string | undefined;
480
482
 
481
483
  // If session has data, try to restore model from it
482
- if (!model && hasExistingSession && existingSession.model) {
483
- const restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);
484
- if (restoredModel && (await modelRegistry.getApiKey(restoredModel))) {
485
- model = restoredModel;
486
- }
487
- if (!model) {
488
- modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;
484
+ const defaultModelStr = existingSession.models.default;
485
+ if (!model && hasExistingSession && defaultModelStr) {
486
+ const slashIdx = defaultModelStr.indexOf("/");
487
+ if (slashIdx > 0) {
488
+ const provider = defaultModelStr.slice(0, slashIdx);
489
+ const modelId = defaultModelStr.slice(slashIdx + 1);
490
+ const restoredModel = modelRegistry.find(provider, modelId);
491
+ if (restoredModel && (await modelRegistry.getApiKey(restoredModel))) {
492
+ model = restoredModel;
493
+ }
494
+ if (!model) {
495
+ modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
496
+ }
489
497
  }
490
498
  }
491
499
 
492
500
  // If still no model, try settings default
493
501
  if (!model) {
494
- const defaultProvider = settingsManager.getDefaultProvider();
495
- const defaultModelId = settingsManager.getDefaultModel();
496
- if (defaultProvider && defaultModelId) {
497
- const settingsModel = modelRegistry.find(defaultProvider, defaultModelId);
498
- if (settingsModel && (await modelRegistry.getApiKey(settingsModel))) {
499
- model = settingsModel;
502
+ const settingsDefaultModel = settingsManager.getModelRole("default");
503
+ if (settingsDefaultModel) {
504
+ const slashIdx = settingsDefaultModel.indexOf("/");
505
+ if (slashIdx > 0) {
506
+ const provider = settingsDefaultModel.slice(0, slashIdx);
507
+ const modelId = settingsDefaultModel.slice(slashIdx + 1);
508
+ const settingsModel = modelRegistry.find(provider, modelId);
509
+ if (settingsModel && (await modelRegistry.getApiKey(settingsModel))) {
510
+ model = settingsModel;
511
+ }
500
512
  }
501
513
  }
502
514
  }
@@ -566,7 +578,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
566
578
  const sessionContext = {
567
579
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
568
580
  };
569
- const builtInTools = options.tools ?? createCodingTools(cwd, options.hasUI ?? false, sessionContext);
581
+ const builtInTools =
582
+ options.tools ??
583
+ createCodingTools(cwd, options.hasUI ?? false, sessionContext, {
584
+ lspFormatOnWrite: settingsManager.getLspFormatOnWrite(),
585
+ lspDiagnosticsOnWrite: settingsManager.getLspDiagnosticsOnWrite(),
586
+ lspDiagnosticsOnEdit: settingsManager.getLspDiagnosticsOnEdit(),
587
+ editFuzzyMatch: settingsManager.getEditFuzzyMatch(),
588
+ });
570
589
  time("createCodingTools");
571
590
 
572
591
  let customToolsResult: CustomToolsLoadResult;
@@ -596,10 +615,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
596
615
  let mcpManager: MCPManager | undefined;
597
616
  const enableMCP = options.enableMCP ?? true;
598
617
  if (enableMCP) {
599
- const mcpResult = await discoverAndLoadMCPTools(cwd);
618
+ const mcpResult = await discoverAndLoadMCPTools(cwd, {
619
+ onConnecting: (serverNames) => {
620
+ if (options.hasUI && serverNames.length > 0) {
621
+ process.stderr.write(`\x1b[90mConnecting to MCP servers: ${serverNames.join(", ")}...\x1b[0m\n`);
622
+ }
623
+ },
624
+ enableProjectConfig: settingsManager.getMCPProjectConfigEnabled(),
625
+ // Always filter Exa - we have native integration
626
+ filterExa: true,
627
+ });
600
628
  time("discoverAndLoadMCPTools");
601
629
  mcpManager = mcpResult.manager;
602
630
 
631
+ // If we extracted Exa API keys from MCP configs and EXA_API_KEY isn't set, use the first one
632
+ if (mcpResult.exaApiKeys.length > 0 && !process.env.EXA_API_KEY) {
633
+ process.env.EXA_API_KEY = mcpResult.exaApiKeys[0];
634
+ }
635
+
603
636
  // Log MCP errors
604
637
  for (const { path, error } of mcpResult.errors) {
605
638
  console.error(`MCP "${path}": ${error}`);
@@ -714,6 +747,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
714
747
  }
715
748
  : undefined,
716
749
  queueMode: settingsManager.getQueueMode(),
750
+ interruptMode: settingsManager.getInterruptMode(),
717
751
  getToolContext: toolContextStore.getContext,
718
752
  getApiKey: async () => {
719
753
  const currentModel = agent.state.model;
@@ -735,7 +769,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
735
769
  } else {
736
770
  // Save initial model and thinking level for new sessions so they can be restored on resume
737
771
  if (model) {
738
- sessionManager.appendModelChange(model.provider, model.id);
772
+ sessionManager.appendModelChange(`${model.provider}/${model.id}`);
739
773
  }
740
774
  sessionManager.appendThinkingLevelChange(thinkingLevel);
741
775
  }
@@ -753,10 +787,23 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
753
787
  });
754
788
  time("createAgentSession");
755
789
 
790
+ // Warm up LSP servers (connects to detected servers)
791
+ let lspServers: CreateAgentSessionResult["lspServers"];
792
+ if (settingsManager.getLspDiagnosticsOnWrite()) {
793
+ try {
794
+ const result = await warmupLspServers(cwd);
795
+ lspServers = result.servers;
796
+ time("warmupLspServers");
797
+ } catch {
798
+ // Ignore warmup errors
799
+ }
800
+ }
801
+
756
802
  return {
757
803
  session,
758
804
  customToolsResult,
759
805
  mcpManager,
760
806
  modelFallbackMessage,
807
+ lspServers,
761
808
  };
762
809
  }