| 1 | using Avalonia.Threading; |
| 2 | using CascadeIDE.Features.IdeMcp.Application; |
| 3 | using CascadeIDE.Features.UiChrome.Application; |
| 4 | using CascadeIDE.Features.Workspace.Application; |
| 5 | using CascadeIDE.Features.UiChrome; |
| 6 | using CascadeIDE.Models; |
| 7 | |
| 8 | namespace CascadeIDE.ViewModels; |
| 9 | |
| 10 | /// <summary>Git + workspace UI.</summary> |
| 11 | public partial class MainWindowViewModel |
| 12 | { |
| 13 | internal static string NormalizeUiMode(string? mode) => UiChromeViewModel.NormalizeUiMode(mode); |
| 14 | |
| 15 | private Task RefreshGitSummaryAsync() => Chrome.RefreshGitSummaryAsync(RunGitCommandAsync); |
| 16 | |
| 17 | /// <summary>Git для полоски Workspace Health (<see cref="RefreshGitSummaryAsync"/>); MCP git — <see cref="IdeMcpGitWorkspaceSession"/>.</summary> |
| 18 | private async Task<(bool Success, int ExitCode, string Output)> RunGitCommandAsync(IReadOnlyList<string> args) |
| 19 | { |
| 20 | var workspace = GetWorkspacePath(); |
| 21 | if (string.IsNullOrWhiteSpace(workspace) || !Directory.Exists(workspace)) |
| 22 | return (false, -1, IdeMcpGitOrchestrator.WorkspaceUnavailableMessage()); |
| 23 | return await _gitRunner.RunAsync(args, workspace).ConfigureAwait(false); |
| 24 | } |
| 25 | |
| 26 | private void InitializeAgentUiDefaults() |
| 27 | { |
| 28 | // Keep operation/trace feeds empty until real runtime events arrive. |
| 29 | // This avoids demo-like placeholder content in production UI. |
| 30 | } |
| 31 | |
| 32 | private void RegisterAgentFeedHandlers() |
| 33 | { |
| 34 | FocusPlanItems.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasFocusPlanItems)); |
| 35 | } |
| 36 | |
| 37 | private void ApplyUiModeLayout(string mode, bool persist) |
| 38 | { |
| 39 | var plan = UiModeLayoutApplyProjection.Create(mode, persist); |
| 40 | var spec = plan.Spec; |
| 41 | |
| 42 | IsPfdRegionExpanded = spec.PfdRegionExpanded; |
| 43 | IsBuildOutputVisible = spec.BuildOutputVisible; |
| 44 | IsTerminalVisible = spec.TerminalVisible; |
| 45 | IsMfdRegionExpanded = spec.MfdRegionExpanded; |
| 46 | EditorGroupCount = spec.EditorGroupCount; |
| 47 | IsInstrumentationDockVisible = spec.InstrumentationDockVisible; |
| 48 | |
| 49 | CoerceMfdShellPageToAllowed(); |
| 50 | CurrentMfdShellPage = UiModeLayoutApplyProjection.ResolveMfdPageAfterApply(plan, CurrentMfdShellPage); |
| 51 | |
| 52 | _ = spec.ThemeSlot switch |
| 53 | { |
| 54 | UiModeThemeSlot.PowerCockpit => Services.UiThemeApply.ApplyOnUiThreadAsync(Services.UiThemeApply.GetPowerCockpitConceptThemeJson()), |
| 55 | UiModeThemeSlot.Dark => Services.UiThemeApply.ApplyOnUiThreadAsync(Services.UiThemeApply.GetDarkThemeJson()), |
| 56 | UiModeThemeSlot.CursorLike => Services.UiThemeApply.ApplyOnUiThreadAsync(Services.UiThemeApply.GetCursorLikeThemeJson()), |
| 57 | _ => Services.UiThemeApply.ApplyOnUiThreadAsync(Services.UiThemeApply.GetCursorLikeThemeJson()) |
| 58 | }; |
| 59 | |
| 60 | UiModeLayoutApplyProjection.PersistWorkspaceMode( |
| 61 | plan, |
| 62 | IsPfdRegionExpanded, |
| 63 | IsTerminalVisible, |
| 64 | _settings, |
| 65 | SaveSettingsIfChanged); |
| 66 | } |
| 67 | |
| 68 | internal string GetWorkspacePath() => WorkspaceDirectoryFromSolutionPath.Resolve(Workspace.SolutionPath); |
| 69 | |
| 70 | partial void OnSelectedOllamaModelChanged(string? value) |
| 71 | { |
| 72 | ChatPanel.RefreshSendChatCommandState(); |
| 73 | if (value == InstallNewSentinel) |
| 74 | { |
| 75 | SelectedModelDetails = ""; |
| 76 | return; |
| 77 | } |
| 78 | if (!string.IsNullOrEmpty(value)) |
| 79 | { |
| 80 | LastSelectedRealModel = value; |
| 81 | _settings.Ai.Local.Ollama.Model = value; |
| 82 | SaveSettingsIfChanged(); |
| 83 | _ = LoadModelDetailsAsync(value); |
| 84 | } |
| 85 | else |
| 86 | SelectedModelDetails = ""; |
| 87 | } |
| 88 | |
| 89 | private async Task LoadModelDetailsAsync(string modelName) |
| 90 | { |
| 91 | try |
| 92 | { |
| 93 | var details = await _ollama.GetModelDetailsAsync(modelName).ConfigureAwait(false); |
| 94 | await UiScheduler.Default.InvokeAsync(() => |
| 95 | { |
| 96 | if (SelectedOllamaModel == modelName) |
| 97 | SelectedModelDetails = details?.ToShortString() ?? ""; |
| 98 | }); |
| 99 | } |
| 100 | catch |
| 101 | { |
| 102 | await UiScheduler.Default.InvokeAsync(() => |
| 103 | { |
| 104 | if (SelectedOllamaModel == modelName) |
| 105 | SelectedModelDetails = ""; |
| 106 | }); |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | public async Task RefreshOllamaAsync() |
| 111 | { |
| 112 | OllamaStatus = "Проверка Ollama…"; |
| 113 | OllamaAvailable = await _ollama.IsAvailableAsync(); |
| 114 | if (OllamaAvailable) |
| 115 | { |
| 116 | var names = await _ollama.GetModelNamesAsync(); |
| 117 | OllamaModels.Clear(); |
| 118 | OllamaModelChoices.Clear(); |
| 119 | foreach (var n in names) |
| 120 | { |
| 121 | OllamaModels.Add(n); |
| 122 | OllamaModelChoices.Add(n); |
| 123 | } |
| 124 | OllamaModelChoices.Add(InstallNewSentinel); |
| 125 | var preferred = _settings.Ai.Local.Ollama.Model?.Trim(); |
| 126 | SelectedOllamaModel = !string.IsNullOrEmpty(preferred) && OllamaModels.Contains(preferred) |
| 127 | ? preferred |
| 128 | : OllamaModels.FirstOrDefault(); |
| 129 | if (LastSelectedRealModel is null && OllamaModels.Count > 0) |
| 130 | LastSelectedRealModel = OllamaModels[0]; |
| 131 | OllamaStatus = names.Count > 0 |
| 132 | ? $"Ollama: {names.Count} моделей" |
| 133 | : "Ollama запущен, моделей нет (ollama pull <model>)"; |
| 134 | } |
| 135 | else |
| 136 | { |
| 137 | OllamaModels.Clear(); |
| 138 | OllamaStatus = "Ollama недоступен (localhost:11434). Установи и запусти Ollama."; |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |