Forge
csharp4405de34
1using System.Diagnostics;
2using AgentNotes.Core;
3using CascadeIDE.Cockpit.DataBus;
4using CascadeIDE.Contracts;
5using CascadeIDE.Models;
6using CascadeIDE.Services;
7using CascadeIDE.Services;
8using CascadeIDE.Services.Lsp;
9
10namespace CascadeIDE.Features.EnvironmentReadiness.Application;
11
12/// <summary>
13/// Сборка снимка «готовность окружения» из настроек и проекции <see cref="IdeHostStateChanged"/> (тот же снимок, что на DataBus; без дампа environ).
14/// </summary>
15[ComputingUnit]
16public static class EnvironmentReadinessSnapshotBuilder
17{
18 /// <summary>Сводная строка блока Dev Tools: агент, LSP, dotnet — лампа «DEV» гаснет, если по всем деталям только Ok/Advisory.</summary>
19 public static AnnunciatorLampItem BuildDevToolsSectionRow(IReadOnlyList<AnnunciatorLampItem> devToolsDetailRows)
20 {
21 if (devToolsDetailRows.Count == 0)
22 throw new ArgumentException("Expected at least one detail row.", nameof(devToolsDetailRows));
23
24 var worst = devToolsDetailRows[0].Level;
25 for (var i = 1; i < devToolsDetailRows.Count; i++)
26 worst = WorstAnnunciatorLevel(worst, devToolsDetailRows[i].Level);
27
28 var level = AggregateSectionLampLevelFromWorstChild(worst);
29 var detail = level == AnnunciatorLampLevel.Ok
30 ? ""
31 : "Есть замечания уровня Caution или выше — см. строки ниже.";
32 return new AnnunciatorLampItem(
33 EnvironmentReadinessCellIds.DevToolsSection,
34 "Dev Tools",
35 detail,
36 level,
37 LampShortLabel: "DEV");
38 }
39
40 /// <summary>Ячейка «агент»: режим моста MCP (stdio) / ACP / без внешнего моста (Off = <see cref="AnnunciatorLampLevel.Caution"/>).</summary>
41 private static AnnunciatorLampItem BuildAgentRow(bool isMcpStdioHost, string? activeAiProvider)
42 {
43 if (isMcpStdioHost)
44 {
45 return new AnnunciatorLampItem(
46 EnvironmentReadinessCellIds.Agent,
47 "Агент (MCP)",
48 "Запуск с --mcp-stdio: внешний хост вызывает инструменты этой сессии CascadeIDE (см. MCP-PROTOCOL.md).",
49 AnnunciatorLampLevel.Advisory,
50 LampShortLabel: "MCP");
51 }
52
53 if (string.Equals(activeAiProvider, "CursorACP", StringComparison.Ordinal))
54 {
55 return new AnnunciatorLampItem(
56 EnvironmentReadinessCellIds.Agent,
57 "Агент (ACP)",
58 "Чат через Cursor ACP: сессия cursor-agent и mcpServers из настроек ([mcp], ADR 0048).",
59 AnnunciatorLampLevel.Advisory,
60 LampShortLabel: "ACP");
61 }
62
63 return new AnnunciatorLampItem(
64 EnvironmentReadinessCellIds.Agent,
65 "Агент (нет моста)",
66 "Нет ни --mcp-stdio, ни провайдера Cursor ACP: внешний контур агента к этой IDE не подключён (встроенные провайдеры — без моста к хосту).",
67 AnnunciatorLampLevel.Caution,
68 LampShortLabel: "Off");
69 }
70
71 /// <summary>Переменные окружения и пути, которые IDE читает для agent-notes / knowledge / netcoredbg (значения в UI не показываем).</summary>
72 /// <param name="tryResolveNetcoreDbgWhenUnset">
73 /// Опционально: подмена результата поиска <c>netcoredbg</c>, когда <c>NETCOREDBG_PATH</c> пуста (только для тестов).
74 /// Если параметр не передан — вызывается <see cref="EnvironmentReadinessExecutablePathProbe.TryResolveExecutablePath"/> для имени <c>netcoredbg</c>.
75 /// Если делегат передан — используется только его возврат (в т.ч. <see langword="null"/> = «не найден»), без обращения к реальному PATH.
76 /// </param>
77 public static IReadOnlyList<AnnunciatorLampItem> BuildEnvProbeRows(
78 EnvironmentReadinessEnvSnapshot env,
79 CascadeIdeSettings settings,
80 Func<string?>? tryResolveNetcoreDbgWhenUnset = null) =>
81 [
82 BuildAgentNotesFileRow(env.AgentNotesFile),
83 BuildAgentNotesConfigRow(env.AgentNotesConfigPath, settings),
84 BuildNetcoreDbgRow(env.NetcoreDbgPath, tryResolveNetcoreDbgWhenUnset),
85 ];
86
87 /// <summary>
88 /// Сводная строка блока env: лампа «ENV» гаснет (<see cref="AnnunciatorLampLevel.Ok"/>), если по трём проверкам нет Caution/Critical
89 /// (только Ok и Advisory — опционально не задано).
90 /// </summary>
91 public static AnnunciatorLampItem BuildEnvSectionRow(IReadOnlyList<AnnunciatorLampItem> envProbeRows)
92 {
93 if (envProbeRows.Count != 3)
94 throw new ArgumentOutOfRangeException(nameof(envProbeRows), envProbeRows.Count, "Expected Notes, KB, Dbg.");
95
96 var level = AggregateEnvBlockLevel(envProbeRows[0].Level, envProbeRows[1].Level, envProbeRows[2].Level);
97 var detail = level == AnnunciatorLampLevel.Ok
98 ? ""
99 : "Есть замечания уровня Caution или выше — см. строки ниже.";
100 return new AnnunciatorLampItem(
101 EnvironmentReadinessCellIds.EnvSection,
102 "Переменные окружения",
103 detail,
104 level,
105 LampShortLabel: "ENV");
106 }
107
108 /// <summary>Worst of three env rows, но Advisory не зажигает сводную лампу: только Caution/Critical.</summary>
109 internal static AnnunciatorLampLevel AggregateEnvBlockLevel(
110 AnnunciatorLampLevel notes,
111 AnnunciatorLampLevel canon,
112 AnnunciatorLampLevel dbg) =>
113 AggregateSectionLampLevelFromWorstChild(
114 WorstAnnunciatorLevel(WorstAnnunciatorLevel(notes, canon), dbg));
115
116 private static AnnunciatorLampLevel WorstAnnunciatorLevel(AnnunciatorLampLevel a, AnnunciatorLampLevel b) =>
117 AnnunciatorLevelOrdinal(a) >= AnnunciatorLevelOrdinal(b) ? a : b;
118
119 private static int AnnunciatorLevelOrdinal(AnnunciatorLampLevel l) => l switch
120 {
121 AnnunciatorLampLevel.Ok => 0,
122 AnnunciatorLampLevel.Advisory => 1,
123 AnnunciatorLampLevel.Caution => 2,
124 AnnunciatorLampLevel.Critical => 3,
125 _ => 0,
126 };
127
128 /// <summary>Для сводной лампы секции: Caution/Critical сохраняют уровень, Ok/Advisory — «норма» (лампа не горит).</summary>
129 internal static AnnunciatorLampLevel AggregateSectionLampLevelFromWorstChild(AnnunciatorLampLevel worstChild) =>
130 worstChild is AnnunciatorLampLevel.Caution or AnnunciatorLampLevel.Critical
131 ? worstChild
132 : AnnunciatorLampLevel.Ok;
133
134 private static AnnunciatorLampItem BuildAgentNotesFileRow(string? raw)
135 {
136 const string title = WellKnownEnv.AgentNotesFile;
137 return EnvironmentReadinessPathAcquisition.ClassifyAgentNotesFilePath(raw) switch
138 {
139 AgentNotesFilePathKind.Unset => new AnnunciatorLampItem(
140 EnvironmentReadinessCellIds.AgentNotesFile,
141 title,
142 "Не задана: заметки в workspace/.cascade-ide/agent-notes.md при открытом решении; либо задай эту переменную для одного глобального файла.",
143 AnnunciatorLampLevel.Ok,
144 LampShortLabel: "Notes"),
145 AgentNotesFilePathKind.ParentDirForGlobalFile => new AnnunciatorLampItem(
146 EnvironmentReadinessCellIds.AgentNotesFile,
147 title,
148 "Задана: каталог для глобального файла заметок существует.",
149 AnnunciatorLampLevel.Ok,
150 LampShortLabel: "Notes"),
151 AgentNotesFilePathKind.FileExists => new AnnunciatorLampItem(
152 EnvironmentReadinessCellIds.AgentNotesFile,
153 title,
154 "Задана: глобальный файл заметок существует.",
155 AnnunciatorLampLevel.Ok,
156 LampShortLabel: "Notes"),
157 AgentNotesFilePathKind.ParentMissing => new AnnunciatorLampItem(
158 EnvironmentReadinessCellIds.AgentNotesFile,
159 title,
160 "Родительский каталог для пути не найден — проверь AGENT_NOTES_FILE.",
161 AnnunciatorLampLevel.Caution,
162 LampShortLabel: "Notes"),
163 AgentNotesFilePathKind.InvalidPath => new AnnunciatorLampItem(
164 EnvironmentReadinessCellIds.AgentNotesFile,
165 title,
166 "Некорректный путь в AGENT_NOTES_FILE.",
167 AnnunciatorLampLevel.Critical,
168 LampShortLabel: "Notes"),
169 _ => throw new UnreachableException()
170 };
171 }
172
173 private static AnnunciatorLampItem BuildAgentNotesConfigRow(string? configPath, CascadeIdeSettings settings)
174 {
175 const string title = "agent-notes config (TOML)";
176 return EnvironmentReadinessPathAcquisition.ClassifyAgentNotesConfigPath(configPath) switch
177 {
178 AgentNotesConfigPathKind.Unset => new AnnunciatorLampItem(
179 EnvironmentReadinessCellIds.AgentNotesCanonPath,
180 title,
181 "Не задан: укажи [agent_notes].config_path в settings.toml — тот же файл, что --config в mcp.json для agent-notes-mcp.",
182 AnnunciatorLampLevel.Advisory,
183 LampShortLabel: "KB"),
184 AgentNotesConfigPathKind.FileMissing => new AnnunciatorLampItem(
185 EnvironmentReadinessCellIds.AgentNotesCanonPath,
186 title,
187 "Файл TOML не найден — проверь config_path.",
188 AnnunciatorLampLevel.Caution,
189 LampShortLabel: "KB"),
190 AgentNotesConfigPathKind.InvalidPath => new AnnunciatorLampItem(
191 EnvironmentReadinessCellIds.AgentNotesCanonPath,
192 title,
193 "Некорректный путь в config_path.",
194 AnnunciatorLampLevel.Critical,
195 LampShortLabel: "KB"),
196 AgentNotesConfigPathKind.FileExists => BuildAgentNotesConfigLoadedRow(configPath!, settings),
197 _ => throw new UnreachableException()
198 };
199 }
200
201 private static AnnunciatorLampItem BuildAgentNotesConfigLoadedRow(string configPath, CascadeIdeSettings settings)
202 {
203 const string title = "agent-notes config (TOML)";
204 if (!AgentNotesRuntimeLoader.EnsureInitialized(settings))
205 {
206 var err = AgentNotesRuntimeLoader.LastLoadError ?? "unknown";
207 return new AnnunciatorLampItem(
208 EnvironmentReadinessCellIds.AgentNotesCanonPath,
209 title,
210 $"TOML найден, но не загружен: {err}",
211 AnnunciatorLampLevel.Caution,
212 LampShortLabel: "KB");
213 }
214
215 if (!AgentNotesRuntime.TryGetPrimaryKnowledgeRoot(out var root) || !Directory.Exists(root))
216 {
217 return new AnnunciatorLampItem(
218 EnvironmentReadinessCellIds.AgentNotesCanonPath,
219 title,
220 "TOML загружен, но primary knowledge root из [knowledge] не существует на диске.",
221 AnnunciatorLampLevel.Caution,
222 LampShortLabel: "KB");
223 }
224
225 _ = configPath;
226 return new AnnunciatorLampItem(
227 EnvironmentReadinessCellIds.AgentNotesCanonPath,
228 title,
229 BuildAgentNotesKbOkDetail(root),
230 AnnunciatorLampLevel.Ok,
231 LampShortLabel: "KB");
232 }
233
234 private static string BuildAgentNotesKbOkDetail(string primaryRoot)
235 {
236 var readOnlyCount = AgentNotesRuntime.IsConfigured
237 ? AgentNotesRuntime.Settings.ReadOnlyKnowledgeRoots.Count
238 : 0;
239 return readOnlyCount > 0
240 ? $"TOML загружен; primary KB доступен; read-only roots: {readOnlyCount} (knowledge_root_id, паритет MCP 2.1)."
241 : "TOML загружен; primary knowledge root доступен (паритет agent-notes-mcp 2.1).";
242 }
243
244 private static AnnunciatorLampItem BuildNetcoreDbgRow(string? raw, Func<string?>? tryResolveNetcoreDbgWhenUnset = null)
245 {
246 const string title = WellKnownEnv.NetcoreDbgPath;
247 return EnvironmentReadinessPathAcquisition.ClassifyNetcoreDbgPath(raw, tryResolveNetcoreDbgWhenUnset) switch
248 {
249 NetcoreDbgPathKind.UnsetFoundOnPath => new AnnunciatorLampItem(
250 EnvironmentReadinessCellIds.NetcoreDbgPath,
251 title,
252 "Не задана: netcoredbg найден в PATH.",
253 AnnunciatorLampLevel.Ok,
254 LampShortLabel: "Dbg"),
255 NetcoreDbgPathKind.UnsetNotOnPath => new AnnunciatorLampItem(
256 EnvironmentReadinessCellIds.NetcoreDbgPath,
257 title,
258 "Не задана: netcoredbg в PATH не найден — задай NETCOREDBG_PATH или установи netcoredbg.",
259 AnnunciatorLampLevel.Advisory,
260 LampShortLabel: "Dbg"),
261 NetcoreDbgPathKind.ExplicitResolved => new AnnunciatorLampItem(
262 EnvironmentReadinessCellIds.NetcoreDbgPath,
263 title,
264 "Исполняемый файл найден (существующий путь или имя в PATH).",
265 AnnunciatorLampLevel.Ok,
266 LampShortLabel: "Dbg"),
267 NetcoreDbgPathKind.InvalidPath => new AnnunciatorLampItem(
268 EnvironmentReadinessCellIds.NetcoreDbgPath,
269 title,
270 "Некорректный путь в NETCOREDBG_PATH.",
271 AnnunciatorLampLevel.Critical,
272 LampShortLabel: "Dbg"),
273 NetcoreDbgPathKind.ExplicitBareNameNotInPath => new AnnunciatorLampItem(
274 EnvironmentReadinessCellIds.NetcoreDbgPath,
275 title,
276 "Имя без полного пути: в каталогах PATH исполняемый файл не найден.",
277 AnnunciatorLampLevel.Caution,
278 LampShortLabel: "Dbg"),
279 NetcoreDbgPathKind.ExplicitFilePathMissing => new AnnunciatorLampItem(
280 EnvironmentReadinessCellIds.NetcoreDbgPath,
281 title,
282 "Файл по NETCOREDBG_PATH не найден.",
283 AnnunciatorLampLevel.Caution,
284 LampShortLabel: "Dbg"),
285 _ => throw new UnreachableException()
286 };
287 }
288
289 /// <summary>Статическая часть: C# LSP, Markdown LSP (без сетевого вызова). Состояние — <see cref="IdeHostStateChanged"/> (тот же снимок, что на DataBus для IDE Health).</summary>
290 public static IReadOnlyList<AnnunciatorLampItem> BuildLspRows(
291 CascadeIdeSettings settings,
292 string? solutionPath,
293 in IdeHostStateChanged lsp)
294 {
295 var list = new List<AnnunciatorLampItem>(4);
296 list.Add(BuildCSharpRow(settings, solutionPath, lsp.CSharpLspHostPresent, lsp.CSharpLspProcessActive));
297 list.Add(BuildMarkdownRow(settings, solutionPath, lsp.MarkdownLspHostPresent, lsp.MarkdownLspProcessActive));
298 return list;
299 }
300
301 /// <summary>Проверка <c>dotnet</c> в PATH (как при сборке).</summary>
302 public static async Task<AnnunciatorLampItem> ProbeDotnetAsync(CancellationToken cancellationToken = default)
303 {
304 var r = await DotnetSdkVersionProbe.RunAsync(cancellationToken).ConfigureAwait(false);
305 if (r.Outcome == DotnetSdkVersionProbeOutcome.Success && !string.IsNullOrWhiteSpace(r.Version))
306 {
307 return new AnnunciatorLampItem(
308 EnvironmentReadinessCellIds.DotnetSdk,
309 "dotnet (SDK / CLI)",
310 $"Версия: {r.Version}",
311 AnnunciatorLampLevel.Ok,
312 LampShortLabel: ".NET");
313 }
314
315 if (r.Outcome == DotnetSdkVersionProbeOutcome.ProcessNull)
316 {
317 return new AnnunciatorLampItem(
318 EnvironmentReadinessCellIds.DotnetSdk,
319 "dotnet (SDK / CLI)",
320 "Не удалось запустить процесс dotnet.",
321 AnnunciatorLampLevel.Critical,
322 LampShortLabel: ".NET");
323 }
324
325 if (r.Outcome == DotnetSdkVersionProbeOutcome.Exception)
326 {
327 return new AnnunciatorLampItem(
328 EnvironmentReadinessCellIds.DotnetSdk,
329 "dotnet (SDK / CLI)",
330 $"Не удалось выполнить dotnet --version: {r.ExceptionMessage ?? "unknown error"}",
331 AnnunciatorLampLevel.Critical,
332 LampShortLabel: ".NET");
333 }
334
335 // NonZeroExit
336 var tail = string.IsNullOrWhiteSpace(r.StdErr) ? $"код выхода {r.ExitCode}" : r.StdErr;
337 return new AnnunciatorLampItem(
338 EnvironmentReadinessCellIds.DotnetSdk,
339 "dotnet (SDK / CLI)",
340 $"dotnet --version не удался ({tail}). Добавь dotnet в PATH или установи SDK.",
341 AnnunciatorLampLevel.Critical,
342 LampShortLabel: ".NET");
343 }
344
345 private static AnnunciatorLampItem BuildCSharpRow(
346 CascadeIdeSettings settings,
347 string? solutionPath,
348 bool hostPresent,
349 bool processActive)
350 {
351 var csharpLsp = settings.Languages.CSharp.ResolveForRuntime();
352 var provider = csharpLsp.Mode;
353
354 if (string.Equals(provider, CSharpLspProviderIds.ParseOnly, StringComparison.OrdinalIgnoreCase))
355 {
356 return new AnnunciatorLampItem(
357 EnvironmentReadinessCellIds.CSharpLsp,
358 "C# LSP",
359 "Режим «только парсер»: отдельный процесс language server не используется (Roslyn в процессе IDE).",
360 AnnunciatorLampLevel.Advisory,
361 LampShortLabel: "C#");
362 }
363
364 if (!EnvironmentReadinessFileFacts.SolutionPathIsExistingFile(solutionPath))
365 {
366 return new AnnunciatorLampItem(
367 EnvironmentReadinessCellIds.CSharpLsp,
368 "C# LSP",
369 $"Провайдер: {provider}. Открой файл решения (.sln/.slnx), чтобы IDE могла запустить LSP.",
370 AnnunciatorLampLevel.Caution,
371 LampShortLabel: "C#");
372 }
373
374 var (exe, _) = CSharpLspProviderIds.ResolveProcessArgs(
375 provider,
376 solutionPath,
377 csharpLsp.Executable,
378 csharpLsp.Arguments);
379
380 var exeHint = string.IsNullOrWhiteSpace(exe) ? provider : $"{provider}: {exe}";
381 exeHint += FormatPendingSettingsEnvHint(settings.Languages.CSharp, provider);
382
383 if (!hostPresent)
384 {
385 return new AnnunciatorLampItem(
386 EnvironmentReadinessCellIds.CSharpLsp,
387 "C# LSP",
388 $"{exeHint}. Процесс не запущен — проверь путь к исполняемому файлу и аргументы в настройках.",
389 AnnunciatorLampLevel.Caution,
390 LampShortLabel: "C#");
391 }
392
393 if (processActive)
394 {
395 return new AnnunciatorLampItem(
396 EnvironmentReadinessCellIds.CSharpLsp,
397 "C# LSP",
398 $"{exeHint}. Процесс активен.",
399 AnnunciatorLampLevel.Ok,
400 LampShortLabel: "C#");
401 }
402
403 return new AnnunciatorLampItem(
404 EnvironmentReadinessCellIds.CSharpLsp,
405 "C# LSP",
406 $"{exeHint}. Процесс не активен (завершился или не прошёл handshake).",
407 AnnunciatorLampLevel.Caution,
408 LampShortLabel: "C#");
409 }
410
411 private static AnnunciatorLampItem BuildMarkdownRow(
412 CascadeIdeSettings settings,
413 string? solutionPath,
414 bool hostPresent,
415 bool processActive)
416 {
417 var markdownLsp = settings.Languages.Markdown.ResolveForRuntime();
418 var provider = markdownLsp.Mode;
419
420 if (string.Equals(provider, MarkdownLspProviderIds.Off, StringComparison.OrdinalIgnoreCase))
421 {
422 return new AnnunciatorLampItem(
423 EnvironmentReadinessCellIds.MarkdownLsp,
424 "Markdown LSP",
425 "Выключено (диагностики Markdown из отдельного сервера не используются).",
426 AnnunciatorLampLevel.Advisory,
427 LampShortLabel: "MD");
428 }
429
430 if (!EnvironmentReadinessFileFacts.SolutionPathIsExistingFile(solutionPath))
431 {
432 return new AnnunciatorLampItem(
433 EnvironmentReadinessCellIds.MarkdownLsp,
434 "Markdown LSP",
435 $"Провайдер: {provider}. Нужен открытый файл решения, чтобы запустить сервер.",
436 AnnunciatorLampLevel.Caution,
437 LampShortLabel: "MD");
438 }
439
440 var (exe, args) = MarkdownLspProviderIds.ResolveProcessArgs(
441 provider,
442 markdownLsp.Executable,
443 markdownLsp.Arguments);
444
445 var argHint = string.IsNullOrWhiteSpace(args) ? "" : $" {args}";
446 var exeHint = string.IsNullOrWhiteSpace(exe) ? $"{provider}" : $"{provider}: {exe}{argHint}";
447 exeHint += FormatPendingSettingsEnvHint(settings.Languages.Markdown, provider);
448
449 if (!hostPresent)
450 {
451 return new AnnunciatorLampItem(
452 EnvironmentReadinessCellIds.MarkdownLsp,
453 "Markdown LSP",
454 $"{exeHint}. Процесс не запущен — проверь установку (например Marksman в PATH) или путь в настройках.",
455 AnnunciatorLampLevel.Caution,
456 LampShortLabel: "MD");
457 }
458
459 if (processActive)
460 {
461 return new AnnunciatorLampItem(
462 EnvironmentReadinessCellIds.MarkdownLsp,
463 "Markdown LSP",
464 $"{exeHint}. Процесс активен.",
465 AnnunciatorLampLevel.Ok,
466 LampShortLabel: "MD");
467 }
468
469 return new AnnunciatorLampItem(
470 EnvironmentReadinessCellIds.MarkdownLsp,
471 "Markdown LSP",
472 $"{exeHint}. Процесс не активен.",
473 AnnunciatorLampLevel.Caution,
474 LampShortLabel: "MD");
475 }
476
477 /// <summary>
478 /// Полный набор строк для страницы «готовность окружения» (ADR 0023): блок Dev Tools (агент, LSP, dotnet), затем env, затем переменные.
479 /// Дополнительные проверки добавлять сюда, чтобы не раздувать ViewModel.
480 /// </summary>
481 public static async Task<IReadOnlyList<AnnunciatorLampItem>> BuildAllRowsAsync(
482 CascadeIdeSettings settings,
483 string? solutionPath,
484 IdeHostStateChanged lsp,
485 bool isMcpStdioHost = false,
486 string? activeAiProvider = null,
487 CancellationToken cancellationToken = default)
488 {
489 var agent = BuildAgentRow(isMcpStdioHost, activeAiProvider);
490 var envRows = BuildEnvProbeRows(EnvironmentReadinessEnvSnapshot.FromSettings(settings), settings);
491 var lspRows = BuildLspRows(settings, solutionPath, lsp);
492 var dotnet = await ProbeDotnetAsync(cancellationToken).ConfigureAwait(false);
493
494 var devToolDetails = new List<AnnunciatorLampItem>(1 + lspRows.Count + 1) { agent };
495 devToolDetails.AddRange(lspRows);
496 devToolDetails.Add(dotnet);
497
498 var devToolsSection = BuildDevToolsSectionRow(devToolDetails);
499 var envSection = BuildEnvSectionRow(envRows);
500
501 var combined = new List<AnnunciatorLampItem>(devToolDetails.Count + envRows.Count + 2);
502 combined.Add(devToolsSection);
503 combined.AddRange(devToolDetails);
504 combined.Add(envSection);
505 combined.AddRange(envRows);
506 return combined;
507 }
508
509 private static string FormatPendingSettingsEnvHint(CSharpLanguageServerSettings csharp, string mode)
510 {
511 var profile = mode switch
512 {
513 "OmniSharp" => csharp.OmniSharp,
514 "CSharpLs" => csharp.CSharpLs,
515 "Custom" => csharp.Custom,
516 _ => csharp.ParseOnly,
517 };
518 return FormatPendingEnvVar(profile.ExecutableEnv);
519 }
520
521 private static string FormatPendingSettingsEnvHint(MarkdownLanguageServerSettings markdown, string mode)
522 {
523 var profile = mode switch
524 {
525 "Marksman" => markdown.Marksman,
526 "Custom" => markdown.Custom,
527 _ => markdown.Off,
528 };
529 return FormatPendingEnvVar(profile.ExecutableEnv);
530 }
531
532 private static string FormatPendingEnvVar(string? environmentVariableName)
533 {
534 if (string.IsNullOrWhiteSpace(environmentVariableName)
535 || SettingsEnvResolver.IsPathLookupSentinel(environmentVariableName))
536 return "";
537
538 return SettingsEnvResolver.TryGetEnvironmentValue(environmentVariableName) is null
539 ? $" (ожидается {environmentVariableName.Trim()})"
540 : "";
541 }
542}
543
View only · write via MCP/CIDE