Forge
csharpdeeb25a2
1using System.ComponentModel;
2using System.Text.Json;
3using Avalonia.Threading;
4using CascadeIDE.Features.AutonomousAgent;
5using CascadeIDE.Features.Build;
6using CascadeIDE.Features.Chat;
7using CascadeIDE.Features.Debug;
8using CascadeIDE.Features.Documents;
9using CascadeIDE.Features.Git;
10using CascadeIDE.Features.Instrumentation;
11using CascadeIDE.Features.Terminal;
12using CascadeIDE.Cockpit.Channels.Eicas;
13using CascadeIDE.Cockpit.Channels.EnvironmentReadiness;
14using CascadeIDE.Cockpit.Channels.WorkspaceHealth;
15using CascadeIDE.Cockpit.ComputingUnits.IdeHealth;
16using CascadeIDE.Cockpit.DataBus;
17using CascadeIDE.Contracts;
18using CascadeIDE.Cockpit.Composition.EnvironmentReadiness;
19using CascadeIDE.Cockpit.Composition.WorkspaceHealth;
20using CascadeIDE.Cockpit.Composition.HostSurface;
21using CascadeIDE.Features.UiChrome;
22using CascadeIDE.Features.HybridIndex.Application;
23using CascadeIDE.Features.Shell.Application;
24using CascadeIDE.Models;
25using CascadeIDE.Features.Os.DataAcquisition;
26using CascadeIDE.Features.IdeMcp.Execution;
27using CascadeIDE.Features.WorkspaceNavigation.Application;
28using CascadeIDE.Features.Workspace;
29using CascadeIDE.Features.Workspace.Application;
30
31namespace CascadeIDE.ViewModels;
32
33/// <summary>
34/// Главный композитор окна (partial-класс, несколько <c>MainWindowViewModel*.cs</c>).
35/// Карта файлов и ответственности — <c>docs/architecture-migration.md</c>, раздел «Срез MainWindowViewModel».
36/// </summary>
37[DataBusPublisher("ide-health & related domain signals")]
38public partial class MainWindowViewModel : ViewModelBase, IAutonomousAgentSessionHost,
39 IMainWindowHostSurfaceInput, SolutionLoadSessionApplyProjection.IHost,
40 IMainWindowMcpHostContext, IWorkspaceNavigationMapHost
41{
42 public const string InstallNewSentinel = "— Установить модель… —";
43
44 private readonly Services.IOllamaService _ollama = new Services.OllamaService();
45 private readonly Services.AiProviderManager _aiProviderManager;
46 private readonly CascadeIdeSettings _settings = Services.SettingsService.Load();
47 internal CascadeIdeSettings GetCascadeSettingsForExecutor() => _settings;
48 private AiKeys _aiKeys = Services.AiKeysStorage.Load();
49 private CascadeIdeSettings? _lastSavedSettings;
50 private AiKeys? _lastSavedAiKeys;
51
52 /// <summary>ADR 0083: значения <c>ai.mode</c> в TOML.</summary>
53 public static readonly IReadOnlyList<string> AiModeOptions = ["local", "acp", "mcp_only", "cloud"];
54
55 public IReadOnlyList<string> AiModeOptionsList => AiModeOptions;
56
57 /// <summary>Для <c>mode = cloud</c>: <c>ai.cloud.active_provider</c>.</summary>
58 public static readonly IReadOnlyList<string> AiCloudProviderKeys = ["anthropic", "openai", "deepseek"];
59
60 public IReadOnlyList<string> AiCloudProviderKeysList => AiCloudProviderKeys;
61
62 private readonly Services.CSharpLanguageService _csharpLanguageService;
63 private readonly Services.ContextMinimizer _contextMinimizer;
64 private readonly Services.WorkspaceDiagnosticsCoordinator _workspaceDiagnostics;
65 private readonly Services.Capabilities.SimpleCapabilityRegistry _capabilities = new();
66 private CSharpLspDiagnosticsHost? _csharpLspHost;
67 private MarkdownLspDiagnosticsHost? _markdownLspHost;
68 private readonly Features.IdeMcp.Application.MainWindowIdeMcpHost _ideMcpHost;
69 private readonly Services.IdeDapDebugSession _dapDebug;
70 private readonly IDataBus _ideDataBus;
71 private readonly DotNetBuildTest.Core.BuildTestJobService _buildTestJobService;
72 private readonly Features.Agent.Environment.IAgentEnvironmentService _agentEnvironment;
73 private readonly HybridIndexOrchestrator _hybridIndex;
74 private readonly IIdeHealthChannel _workspaceHealth;
75 private readonly IIdeHealthSurfaceCompositor _workspaceHealthSurfaceCompositor;
76 private readonly IEicasFeed _eicasFeed;
77 private readonly IEnvironmentReadinessChannel _environmentReadinessChannel;
78 private readonly IEnvironmentReadinessSurfaceCompositor _environmentReadinessSurfaceCompositor;
79 private readonly Services.Presentation.PresentationParseResult _presentationParse;
80 private readonly bool _presentationDedicatedMfdSecondScreen;
81 private readonly bool _presentationTripleOneAnchorPerZone;
82 private readonly bool _presentationMfdHostTopology;
83 private readonly bool _presentationPmForwardTwoScreen;
84 private readonly bool _presentationPmHostTopology;
85 private readonly IInstrumentMountPolicyResolver _instrumentMountPolicyResolver;
86 private bool _suppressMfdColumnForMfdHostWindow;
87 private bool _suppressPfdColumnForPfdHostWindow;
88
89 private Services.McpClientService _mcpClientService;
90 private AutonomousAgentService _autonomousAgentService;
91
92 private readonly IOsShellLauncher _osShell;
93
94 /// <summary>DAP-сессия (netcoredbg): launch/attach и обновление панели отладки.</summary>
95 public Services.IdeDapDebugSession DapDebug => _dapDebug;
96
97 /// <summary>Solution/workspace state and background loading.</summary>
98 public SolutionWorkspaceViewModel Workspace { get; }
99
100 /// <summary>Git-строки для Workspace Health и bloom при смене UI-режима.</summary>
101 public UiChromeViewModel Chrome { get; }
102
103 /// <summary>Открытые файлы, группы редакторов и Dock.</summary>
104 public DocumentsWorkspaceViewModel Documents { get; }
105
106 /// <summary>Автономный агент (Power): цель, шаги, start/pause/resume.</summary>
107 public AutonomousAgentSessionViewModel Autonomous { get; }
108
109 private void OnDocumentsPropertyChanged(object? _, PropertyChangedEventArgs e)
110 {
111 if (e.PropertyName is null)
112 return;
113 OnPropertyChanged(e.PropertyName);
114 if (e.PropertyName == nameof(DocumentsWorkspaceViewModel.SelectedDocumentGroup2))
115 OnPropertyChanged(nameof(EditorTextGroup2));
116 if (e.PropertyName == nameof(DocumentsWorkspaceViewModel.SelectedDocumentGroup3))
117 OnPropertyChanged(nameof(EditorTextGroup3));
118 }
119
120 private void OnWorkspacePropertyChanged(string? propertyName)
121 {
122 switch (propertyName)
123 {
124 case nameof(SolutionWorkspaceViewModel.SolutionPath):
125 OnPropertyChanged(nameof(BreakpointLinesInCurrentFile));
126 OnPropertyChanged(nameof(AllBreakpointLinesInCurrentFile));
127 BuildSolutionCommand.NotifyCanExecuteChanged();
128 ImportLaunchSettingsFromSelectionCommand.NotifyCanExecuteChanged();
129 HandleSolutionPathChanged(Workspace.SolutionPath);
130 break;
131 case nameof(SolutionWorkspaceViewModel.SelectedSolutionItem):
132 HandleSelectedSolutionItemChanged(Workspace.SelectedSolutionItem);
133 SetStartupProjectFromSelectionCommand.NotifyCanExecuteChanged();
134 ImportLaunchSettingsFromSelectionCommand.NotifyCanExecuteChanged();
135 break;
136 }
137 }
138
139 /// <summary>Событие: перейти в активном редакторе на строку/колонку (1-based) после открытия файла.</summary>
140 public event Action<int, int>? GotoActiveEditorLineColumnRequested;
141
142 private void NavigateToProblemFromList(ProblemListItem item)
143 {
144 if (string.IsNullOrWhiteSpace(item.FilePath))
145 return;
146 Documents.OpenOrActivateDocument(item.FilePath);
147 var line = item.Line;
148 var col = item.Column;
149 UiScheduler.Default.Post(() => GotoActiveEditorLineColumnRequested?.Invoke(line, col), DispatcherPriority.Loaded);
150 }
151
152 private AutonomousAgentService CreateAutonomousAgentService(Services.McpClientService mcpClientService) =>
153 new AutonomousAgentService(
154 _aiProviderManager,
155 IdeMcp,
156 mcpClientService,
157 () => ActiveAiProvider,
158 () => SelectedOllamaModel,
159 () => UseMinimizedContext,
160 () => CurrentFilePath,
161 () => EditorText,
162 (kind, text, status, at) => InstrumentationPanel.AppendAgentTraceStep(kind, text, status, at),
163 msg => UiScheduler.Default.Post(() => InstrumentationPanel.EventTimeline.Insert(0, msg)),
164 _agentEnvironment.Orchestrator);
165
166 /// <summary>
167 /// Полоса Workspace Health читает счётчики отладки с главного VM; при смене MCP-стека обновляем строки.
168 /// </summary>
169 private void OnInstrumentationPanelPropertyChanged(object? sender, PropertyChangedEventArgs e)
170 {
171 if (e.PropertyName != nameof(InstrumentationPanelViewModel.IsDebugPanelVisible))
172 return;
173 OnPropertyChanged(nameof(IdeHealthDebugText));
174 OnPropertyChanged(nameof(IdeHealthDebugCockpitShort));
175 }
176
177 /// <summary>Вывод сборки (нижняя вкладка Build output).</summary>
178 public BuildOutputPanelViewModel BuildOutputPanel { get; }
179
180 /// <summary>Ошибки и предупреждения по открытым .cs (Roslyn).</summary>
181 public ProblemsPanelViewModel ProblemsPanel { get; }
182
183 /// <summary>Markdown preview как shared state для MFD page/tool.</summary>
184 public MarkdownPreviewToolViewModel MarkdownPreviewTool { get; }
185
186 /// <summary>Терминал (нижняя вкладка Terminal).</summary>
187 public TerminalPanelViewModel TerminalPanel { get; }
188
189 /// <summary>Панель Git (нижняя вкладка); состояние и команды вынесены из <see cref="MainWindowViewModel"/>.</summary>
190 public GitPanelViewModel GitPanel { get; }
191
192 /// <summary>Чат с LLM (правая колонка): история, ввод, отправка.</summary>
193 public ChatPanelViewModel ChatPanel { get; }
194
195 /// <summary>Инструментирование: трасса агента, события, тесты, стек MCP-отладки. В разметке — <c>DataContext="{Binding InstrumentationPanel}"</c>.</summary>
196 public InstrumentationPanelViewModel InstrumentationPanel { get; }
197
198 /// <summary>Гипотезы отладки (JSON в workspace). Вкладка нижней панели в режиме Debug.</summary>
199 public HypothesesPanelViewModel HypothesesPanel { get; }
200
201 /// <summary>Общий экземпляр Roslyn для редактора, контекста чата и диагностик.</summary>
202 public Services.CSharpLanguageService CSharpLanguage => _csharpLanguageService;
203
204 /// <summary>Кэш диагностик по открытым .cs и панель Problems.</summary>
205 public Services.WorkspaceDiagnosticsCoordinator WorkspaceDiagnostics => _workspaceDiagnostics;
206
207 private void SaveSettingsIfChanged()
208 {
209 if (_lastSavedSettings is null || !_settings.Is(_lastSavedSettings))
210 {
211 Services.SettingsService.Save(_settings);
212 _lastSavedSettings = (CascadeIdeSettings)_settings.Clone();
213 }
214 }
215
216 private void SaveAiKeysIfChanged()
217 {
218 if (_lastSavedAiKeys is null || !_aiKeys.Is(_lastSavedAiKeys))
219 {
220 Services.AiKeysStorage.Save(_aiKeys);
221 _lastSavedAiKeys = (AiKeys)_aiKeys.Clone();
222 }
223 }
224
225 private Microsoft.Extensions.AI.IChatClient? TryCreateCloudMafIChatClientForChatPanel()
226 {
227 return ActiveAiProvider switch
228 {
229 "Anthropic" => Services.CascadeIdeMafChatClientFactories.CreateAnthropicChatClientOrNull(
230 AnthropicApiKey,
231 _settings.Ai.Cloud.Anthropic.Model),
232 "OpenAI" => Services.CascadeIdeMafChatClientFactories.CreateOpenAiCompatibleChatClientOrNull(
233 OpenAiApiKey,
234 _settings.Ai.Cloud.OpenAi.BaseUrl,
235 _settings.Ai.Cloud.OpenAi.Model),
236 "DeepSeek" => Services.CascadeIdeMafChatClientFactories.CreateOpenAiCompatibleChatClientOrNull(
237 DeepSeekApiKey,
238 _settings.Ai.Cloud.DeepSeek.BaseUrl,
239 _settings.Ai.Cloud.DeepSeek.Model),
240 _ => null,
241 };
242 }
243
244 private (Services.IAiChatProvider? Provider, string Model) ResolveProvider(string providerKey)
245 {
246 var key = providerKey ?? _settings.Ai.ResolveEffectiveProviderUiKey();
247 return key switch
248 {
249 "Anthropic" => (new Services.AnthropicProvider(_aiKeys.AnthropicApiKey ?? "", _settings.Ai.Cloud.Anthropic.Model), _settings.Ai.Cloud.Anthropic.Model),
250 "OpenAI" => (new Services.OpenAiCompatibleProvider(_settings.Ai.Cloud.OpenAi.BaseUrl, _aiKeys.OpenAiApiKey ?? "", _settings.Ai.Cloud.OpenAi.Model), _settings.Ai.Cloud.OpenAi.Model),
251 "DeepSeek" => (new Services.OpenAiCompatibleProvider(_settings.Ai.Cloud.DeepSeek.BaseUrl, _aiKeys.DeepSeekApiKey ?? "", _settings.Ai.Cloud.DeepSeek.Model), _settings.Ai.Cloud.DeepSeek.Model),
252 "CursorACP" => (null, ""),
253 _ => (new Services.OllamaProvider(_ollama), SelectedOllamaModel ?? _settings.Ai.Local.Ollama.Model)
254 };
255 }
256
257 private Features.Chat.FmOpenAiCompatibleCredentials? ResolveFmOpenAiCredentialsForCatalog() =>
258 ActiveAiProvider switch
259 {
260 "OpenAI" when !string.IsNullOrWhiteSpace(OpenAiApiKey) => new(
261 _settings.Ai.Cloud.OpenAi.BaseUrl,
262 OpenAiApiKey.Trim(),
263 _settings.Ai.Cloud.OpenAi.Model),
264 "DeepSeek" when !string.IsNullOrWhiteSpace(DeepSeekApiKey) => new(
265 _settings.Ai.Cloud.DeepSeek.BaseUrl,
266 DeepSeekApiKey.Trim(),
267 _settings.Ai.Cloud.DeepSeek.Model),
268 _ => null,
269 };
270
271 private readonly Services.AppDataService _appData = new();
272 private readonly IGitCommandRunner _gitRunner = new GitCommandRunner();
273 private readonly IDotnetCommandRunner _dotnetRunner = new DotnetCommandRunner();
274 private readonly Services.McpDotnetBuildTestService _mcpBuildTest;
275 private readonly Services.McpAgentNotesService _mcpAgentNotes;
276
277 private CancellationTokenSource? _openFileDebounceCts;
278 // Solution load version is owned by Workspace.
279 private string? _lastBuildBinlogPath;
280
281 /// <summary>Краткий список языков с подсветкой в редакторе (для окна настроек).</summary>
282 public string SupportedEditorLanguagesSummary => Services.EditorLanguageSupport.GetSummary();
283
284 public void LoadSendMessageKeyFromStorage()
285 {
286 var stored = _appData.Get("SendMessageKey");
287 if (!string.IsNullOrEmpty(stored) && SendMessageKeyOptions.Contains(stored))
288 SendMessageKey = stored;
289 }
290
291 public void LoadComposerNewLineKeyFromStorage()
292 {
293 var stored = _appData.Get("ComposerNewLineKey");
294 if (!string.IsNullOrEmpty(stored) && SendMessageKeyOptions.Contains(stored))
295 ComposerNewLineKey = stored;
296 else
297 ComposerNewLineKey = Features.Chat.ChatComposerChordOptions.ComplementaryChord(SendMessageKey);
298
299 NormalizeChatEnterChordPair();
300 }
301
302 private bool _suppressChatEnterChordPair;
303
304 /// <summary>Совпадение send/newline запрещено: сдвигаем перенос строки.</summary>
305 private void NormalizeChatEnterChordPair()
306 {
307 if (_suppressChatEnterChordPair)
308 return;
309 if (string.Equals(ComposerNewLineKey, SendMessageKey, StringComparison.Ordinal))
310 {
311 _suppressChatEnterChordPair = true;
312 try
313 {
314 ComposerNewLineKey = Features.Chat.ChatComposerChordOptions.ComplementaryChord(SendMessageKey);
315 }
316 finally
317 {
318 _suppressChatEnterChordPair = false;
319 }
320 }
321 }
322
323 private void HandleSelectedSolutionItemChanged(SolutionItem? value)
324 {
325 _openFileDebounceCts?.Cancel();
326 _openFileDebounceCts = new CancellationTokenSource();
327 var cts = _openFileDebounceCts;
328 _ = Documents.OpenFileAfterDebounceAsync(cts.Token);
329 }
330
331 private void HandleSolutionPathChanged(string value)
332 {
333 if (string.IsNullOrWhiteSpace(value))
334 {
335 ClearStartupProjectInMemoryOnly();
336 RefreshLaunchProfilePickerFromStore();
337 }
338
339 var ws = WorkspaceDirectoryFromSolutionPath.Resolve(value);
340 UiModeCatalog.ApplyRepositoryWorkspaceTomlOverlay(ws);
341 NotifyDockedInstrumentSlotBindings();
342 OnPropertyChanged(nameof(ChatPanelColumnPixelWidth));
343 OnPropertyChanged(nameof(IsChatPanelColumnVisible));
344 OnPropertyChanged(nameof(IsMfdColumnVisible));
345 OnPropertyChanged(nameof(IsPfdRegionExpanded));
346 OnPropertyChanged(nameof(IsPfdColumnVisible));
347
348 ChatPanel.DisposeCursorAcpSession();
349 _ = ChatPanel.ReloadIntercomSessionFromDiskAsync();
350 RefreshIntercomOnSolutionOpen();
351 EnsurePfdBackgroundStatusSubscription();
352 if (string.IsNullOrWhiteSpace(value))
353 _hciReindexPending = false;
354 else
355 MarkHciReindexPendingForPfdStatus();
356
357 ApplySolutionWarmupForCurrentSolution();
358 ApplyHybridCodebaseIndexOrchestrationForCurrentSolution(pokeWhenAutoReindex: true);
359 Editor.AttachBreakpointsFileWatcher(value);
360 _ = RefreshGitSummaryAsync();
361 _ = GitPanel.RefreshRepositoryFlagAsync();
362 if (IsGitPanelVisible)
363 _ = GitPanel.RefreshGitPanelAsync();
364 _ = RestartCSharpLanguageServerAsync();
365 _ = RestartMarkdownLanguageServerAsync();
366 HypothesesPanel.LoadFromWorkspace();
367 RestoreCompactAuxiliaryPanelAfterSolutionScopeChange(value);
368 }
369
370 private void RestoreCompactAuxiliaryPanelAfterSolutionScopeChange(string? solutionPath)
371 {
372 if (!IsCompactPresentationTier || string.IsNullOrWhiteSpace(solutionPath))
373 return;
374
375 ApplyPfdRegionExpanded(false);
376 if (!IsMfdRegionExpanded)
377 ApplyMfdRegionExpanded(true);
378
379 TryNavigateToMfdShellPage(MfdShellPage.Chat);
380 }
381
382 private (string WorkspaceRoot, string? SolutionPath) ResolveHybridIndexScope(string workspaceRoot, string? solutionPath) =>
383 HybridIndexScopeResolver.ApplyScopeMode(_settings.HybridIndex.ScopeMode, workspaceRoot, solutionPath);
384
385 /// <summary>
386 /// ADR 0106: синхронизация оркестратора HCI с <see cref="CascadeIdeSettings.HybridIndex"/> и текущим решением в <see cref="SolutionWorkspaceViewModel"/>.
387 /// </summary>
388 private void ApplyHybridCodebaseIndexOrchestrationForCurrentSolution(bool pokeWhenAutoReindex)
389 {
390 var value = Workspace.SolutionPath ?? "";
391 var ws = WorkspaceDirectoryFromSolutionPath.Resolve(value);
392 if (string.IsNullOrWhiteSpace(ws))
393 return;
394
395 HybridIndexOrchestrationPolicy.ApplyForCurrentScope(
396 _hybridIndex,
397 _settings.HybridIndex,
398 ChatMcpOnly,
399 ws,
400 value,
401 pokeWhenAutoReindex);
402 }
403
404 private string? BuildChatMinimizedContextBlockCore()
405 {
406 string? block = null;
407 if (UseMinimizedContext
408 && !string.IsNullOrEmpty(CurrentFilePath)
409 && !string.IsNullOrEmpty(EditorText))
410 {
411 var minimized = _contextMinimizer.Minimize(CurrentFilePath, EditorText, CancellationToken.None);
412 if (!string.IsNullOrWhiteSpace(minimized))
413 block = minimized;
414 }
415
416 var hot = ChatPanel.Harness.HotContextBlock;
417 var telemetry = ChatPanel.Harness.BuildTelemetryContextBlock(
418 ChatPanel.Harness.GetTelemetry(),
419 _agentEnvironment.EpochTracker.IsUiStale);
420
421 string? prefix = null;
422 if (!string.IsNullOrWhiteSpace(hot))
423 prefix = hot;
424 if (!string.IsNullOrWhiteSpace(telemetry))
425 prefix = string.IsNullOrWhiteSpace(prefix) ? telemetry : prefix + "\n\n---\n\n" + telemetry;
426
427 if (string.IsNullOrWhiteSpace(prefix))
428 return block;
429
430 return string.IsNullOrWhiteSpace(block)
431 ? prefix
432 : prefix + "\n\n---\n\n" + block;
433 }
434
435 /// <summary>MCP / палитра / MAF — единая поверхность команд IDE (Wave 2 Big Bang).</summary>
436 public Services.IIdeMcpActions IdeMcp => _ideMcpHost;
437
438}
439
View only · write via MCP/CIDE