@using LazyBear.MCP.Services.Logging @using LazyBear.MCP.Services.ToolRegistry @inject ToolRegistryService Registry @inject InMemoryLogSink LogSink @inject GlobalKeyboardService KeyboardService @inject LocalizationService Localization @inject IHostApplicationLifetime AppLifetime @implements IDisposable @foreach (var segment in BuildHeaderSegments()) { } @if (_activeTab == Tab.Overview) { } else if (_activeTab == Tab.Logs) { } else { } @code { private const char Nbsp = '\u00A0'; private enum Tab { Overview, Logs, Settings } private readonly record struct HeaderSegment( string Content, Spectre.Console.Color Foreground, Spectre.Console.Color Background, Spectre.Console.Decoration Decoration); private static readonly Tab[] _tabs = [Tab.Overview, Tab.Logs, Tab.Settings]; private static readonly string[] _logFilters = ["All", "Info", "Warn", "Error"]; private readonly HashSet _expandedModules = new(StringComparer.Ordinal); private Tab _activeTab = Tab.Overview; private int _overviewSelection; private int _logFilterIndex; private int _logSelection; private int _settingsSelection; private bool _logsStickToBottom = true; private bool _isHelpOpen; private static readonly string _mcpEndpoint = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://localhost:5000"; private static int GetPanelHeight() => Math.Max(UiMetrics.ConsoleHeight - 4, 10); private static int GetOverviewViewportRows() => Math.Max(GetPanelHeight() - 6, 3); private static int GetLogsViewportRows() => Math.Max(GetPanelHeight() - 10, 5); private static int GetSettingsViewportRows() => Math.Max(GetPanelHeight() - 7, 5); private static int GetHelpModalWidth() => Math.Max(Math.Min(UiMetrics.ConsoleWidth - 6, 84), 36); protected override void OnInitialized() { Registry.StateChanged += OnRegistryChanged; LogSink.OnLog += OnNewLog; KeyboardService.OnKeyPressed += OnConsoleKeyPressed; Localization.OnChanged += OnLocaleChanged; } // Конвертация ConsoleKeyInfo → KeyboardEventArgs для переиспользования существующей логики private static KeyboardEventArgs ConvertKey(ConsoleKeyInfo key) { var name = key.Key switch { ConsoleKey.UpArrow => "ArrowUp", ConsoleKey.DownArrow => "ArrowDown", ConsoleKey.LeftArrow => "ArrowLeft", ConsoleKey.RightArrow => "ArrowRight", ConsoleKey.Enter => "Enter", ConsoleKey.Spacebar => " ", ConsoleKey.Home => "Home", ConsoleKey.End => "End", ConsoleKey.PageUp => "PageUp", ConsoleKey.PageDown => "PageDown", ConsoleKey.Tab => "Tab", ConsoleKey.Escape => "Escape", _ => key.KeyChar == '\0' ? string.Empty : key.KeyChar.ToString() }; return new KeyboardEventArgs { Key = name, ShiftKey = (key.Modifiers & ConsoleModifiers.Shift) != 0 }; } private void OnConsoleKeyPressed(ConsoleKeyInfo key) { var args = ConvertKey(key); if (string.IsNullOrEmpty(args.Key)) { return; } InvokeAsync(() => { HandleKeyDown(args); StateHasChanged(); }); } private void OnLocaleChanged() => InvokeAsync(StateHasChanged); private void HandleKeyDown(KeyboardEventArgs args) { if (args.Key is "h" or "H") { _isHelpOpen = !_isHelpOpen; return; } if (_isHelpOpen) { if (args.Key == "Escape") { _isHelpOpen = false; } return; } if (string.Equals(args.Key, "Tab", StringComparison.Ordinal)) { ChangeTab(args.ShiftKey ? -1 : 1); return; } if (args.Key is "l" or "L") { Localization.SwitchNext(); return; // StateHasChanged вызовет OnLocaleChanged } if (args.Key is "q" or "Q") { AppLifetime.StopApplication(); return; } switch (_activeTab) { case Tab.Overview: HandleOverviewKey(args); break; case Tab.Logs: HandleLogsKey(args); break; case Tab.Settings: HandleSettingsKey(args); break; } } private Task OnOverviewSelectionChanged(int value) { var rows = GetOverviewRows(); _overviewSelection = rows.Count == 0 ? 0 : Math.Clamp(value, 0, rows.Count - 1); return Task.CompletedTask; } private Task OnLogSelectionChanged(int value) { var entries = GetFilteredLogEntries(); _logSelection = entries.Count == 0 ? 0 : Math.Clamp(value, 0, entries.Count - 1); _logsStickToBottom = entries.Count == 0 || _logSelection >= entries.Count - 1; return Task.CompletedTask; } private Task OnSettingsSelectionChanged(int value) { var entries = GetSettingsEntries(); _settingsSelection = entries.Count == 0 ? 0 : Math.Clamp(value, 0, entries.Count - 1); return Task.CompletedTask; } private void ChangeTab(int step) { var currentIndex = Array.IndexOf(_tabs, _activeTab); if (currentIndex < 0) { currentIndex = 0; } var nextIndex = (currentIndex + step + _tabs.Length) % _tabs.Length; _activeTab = _tabs[nextIndex]; ClampSelections(); } private void HandleOverviewKey(KeyboardEventArgs args) { var rows = GetOverviewRows(); if (rows.Count == 0) { _overviewSelection = 0; return; } _overviewSelection = Math.Clamp(_overviewSelection, 0, rows.Count - 1); switch (args.Key) { case "ArrowUp": _overviewSelection = Math.Max(0, _overviewSelection - 1); break; case "ArrowDown": _overviewSelection = Math.Min(rows.Count - 1, _overviewSelection + 1); break; case "Home": _overviewSelection = 0; break; case "End": _overviewSelection = rows.Count - 1; break; case "Enter": _activeTab = Tab.Settings; SelectSettingsModule(rows[_overviewSelection].ModuleName); break; } } private void HandleLogsKey(KeyboardEventArgs args) { switch (args.Key) { case "ArrowLeft": _logFilterIndex = (_logFilterIndex - 1 + _logFilters.Length) % _logFilters.Length; ResetLogsSelectionToBottom(); return; case "ArrowRight": _logFilterIndex = (_logFilterIndex + 1) % _logFilters.Length; ResetLogsSelectionToBottom(); return; } var entries = GetFilteredLogEntries(); if (entries.Count == 0) { _logSelection = 0; _logsStickToBottom = true; return; } _logSelection = Math.Clamp(_logSelection, 0, entries.Count - 1); var page = Math.Max(GetLogsViewportRows() - 1, 1); switch (args.Key) { case "ArrowUp": _logSelection = Math.Max(0, _logSelection - 1); break; case "ArrowDown": _logSelection = Math.Min(entries.Count - 1, _logSelection + 1); break; case "PageUp": _logSelection = Math.Max(0, _logSelection - page); break; case "PageDown": case " ": case "Spacebar": _logSelection = Math.Min(entries.Count - 1, _logSelection + page); break; case "Home": _logSelection = 0; break; case "End": _logSelection = entries.Count - 1; break; } _logsStickToBottom = _logSelection >= entries.Count - 1; } private void HandleSettingsKey(KeyboardEventArgs args) { var entries = GetSettingsEntries(); if (entries.Count == 0) { _settingsSelection = 0; return; } _settingsSelection = Math.Clamp(_settingsSelection, 0, entries.Count - 1); var selected = entries[_settingsSelection]; var page = Math.Max(GetSettingsViewportRows() - 1, 1); switch (args.Key) { case "ArrowUp": _settingsSelection = Math.Max(0, _settingsSelection - 1); return; case "ArrowDown": _settingsSelection = Math.Min(entries.Count - 1, _settingsSelection + 1); return; case "PageUp": _settingsSelection = Math.Max(0, _settingsSelection - page); return; case "PageDown": _settingsSelection = Math.Min(entries.Count - 1, _settingsSelection + page); return; case "Home": _settingsSelection = 0; return; case "End": _settingsSelection = entries.Count - 1; return; case "ArrowRight": ExpandModule(selected); return; case "ArrowLeft": CollapseModuleOrFocusParent(selected); return; case "Enter": ToggleExpansion(selected); return; case " ": case "Spacebar": ToggleSetting(selected); return; } } private void ExpandModule(SettingsEntry entry) { if (entry.Kind != SettingsEntryKind.Module || entry.IsExpanded) { return; } _expandedModules.Add(entry.ModuleName); ClampSelections(); } private void CollapseModuleOrFocusParent(SettingsEntry entry) { if (entry.Kind == SettingsEntryKind.Module) { if (_expandedModules.Remove(entry.ModuleName)) { ClampSelections(); } return; } _expandedModules.Remove(entry.ModuleName); SelectSettingsModule(entry.ModuleName); } private void ToggleExpansion(SettingsEntry entry) { if (entry.Kind != SettingsEntryKind.Module) { ToggleSetting(entry); return; } if (_expandedModules.Contains(entry.ModuleName)) { _expandedModules.Remove(entry.ModuleName); } else { _expandedModules.Add(entry.ModuleName); } SelectSettingsModule(entry.ModuleName); } private void ToggleSetting(SettingsEntry entry) { if (entry.Kind == SettingsEntryKind.Module) { Registry.ToggleModule(entry.ModuleName); return; } if (!string.IsNullOrWhiteSpace(entry.ToolName)) { Registry.ToggleTool(entry.ModuleName, entry.ToolName); } } private void SelectSettingsModule(string moduleName) { var entries = GetSettingsEntries(); var index = entries.FindIndex(entry => entry.Kind == SettingsEntryKind.Module && string.Equals(entry.ModuleName, moduleName, StringComparison.Ordinal)); _settingsSelection = index >= 0 ? index : 0; } private void ResetLogsSelectionToBottom() { var entries = GetFilteredLogEntries(); _logSelection = Math.Max(entries.Count - 1, 0); _logsStickToBottom = true; } private List GetOverviewRows() => Registry.GetModules() .Select(module => { var (configuredTools, totalTools) = Registry.GetConfiguredToolCounts(module.ModuleName); return new OverviewRow( module.ModuleName, module.Description, Registry.IsModuleEnabled(module.ModuleName), configuredTools, totalTools); }) .ToList(); private List GetSettingsEntries() { var entries = new List(); foreach (var module in Registry.GetModules()) { var isModuleEnabled = Registry.IsModuleEnabled(module.ModuleName); var isExpanded = _expandedModules.Contains(module.ModuleName); entries.Add(new SettingsEntry( SettingsEntryKind.Module, module.ModuleName, null, module.ModuleName, module.Description, isModuleEnabled, isModuleEnabled, isExpanded, 0)); if (!isExpanded) { continue; } foreach (var toolName in module.ToolNames) { var isConfigured = Registry.IsToolConfiguredEnabled(module.ModuleName, toolName); entries.Add(new SettingsEntry( SettingsEntryKind.Tool, module.ModuleName, toolName, toolName, isModuleEnabled ? $"{module.ModuleName} / {toolName}" : $"{module.ModuleName} / {toolName} {Localization.Current.ModuleOffPreserved}", isConfigured, isModuleEnabled, false, 1)); } } return entries; } private List GetFilteredLogEntries() { var entries = LogSink.GetEntries(); return _logFilters[_logFilterIndex] switch { "Info" => entries.Where(IsInfoLevel).ToList(), "Warn" => entries.Where(entry => entry.Level == LogLevel.Warning).ToList(), "Error" => entries.Where(entry => entry.Level is LogLevel.Error or LogLevel.Critical).ToList(), _ => entries.ToList() }; } private void ClampSelections() { var overviewRows = GetOverviewRows(); _overviewSelection = overviewRows.Count == 0 ? 0 : Math.Clamp(_overviewSelection, 0, overviewRows.Count - 1); var logEntries = GetFilteredLogEntries(); _logSelection = logEntries.Count == 0 ? 0 : Math.Clamp(_logSelection, 0, logEntries.Count - 1); var settingsEntries = GetSettingsEntries(); _settingsSelection = settingsEntries.Count == 0 ? 0 : Math.Clamp(_settingsSelection, 0, settingsEntries.Count - 1); } private void OnRegistryChanged() { InvokeAsync(() => { ClampSelections(); StateHasChanged(); }); } private void OnNewLog(LogEntry entry) { InvokeAsync(() => { if (_logsStickToBottom && MatchesCurrentLogFilter(entry)) { var filteredEntries = GetFilteredLogEntries(); _logSelection = Math.Max(filteredEntries.Count - 1, 0); } else { ClampSelections(); } StateHasChanged(); }); } private bool MatchesCurrentLogFilter(LogEntry entry) => _logFilters[_logFilterIndex] switch { "Info" => IsInfoLevel(entry), "Warn" => entry.Level == LogLevel.Warning, "Error" => entry.Level is LogLevel.Error or LogLevel.Critical, _ => true }; private static bool IsInfoLevel(LogEntry entry) => entry.Level is LogLevel.Information or LogLevel.Debug or LogLevel.Trace; private string GetTabLabel(Tab tab) => tab switch { Tab.Overview => Localization.Current.TabOverview, Tab.Logs => Localization.Current.TabLogs, _ => Localization.Current.TabSettings }; private HeaderSegment[] BuildHeaderSegments() { var width = Math.Max(UiMetrics.ConsoleWidth, 1); var projectSegmentWidth = GetHeaderProjectSegment().Length; var availableTabsWidth = Math.Max(width - projectSegmentWidth, 0); if (availableTabsWidth <= 0) { return []; } var labels = _tabs.Select(GetTabLabel).ToArray(); var labelWidths = labels.Select(label => label.Length).ToArray(); var availableLabelWidth = availableTabsWidth - (_tabs.Length * 2); if (availableLabelWidth < _tabs.Length) { return [ new HeaderSegment( FillBar(availableTabsWidth), UiPalette.Text, UiPalette.HeaderTabsBackground, Spectre.Console.Decoration.None) ]; } while (labelWidths.Sum() > availableLabelWidth) { var largestIndex = Array.IndexOf(labelWidths, labelWidths.Max()); if (largestIndex < 0 || labelWidths[largestIndex] <= 1) { break; } labelWidths[largestIndex]--; } var segments = new List(_tabs.Length + 1); var usedWidth = 0; for (var i = 0; i < _tabs.Length; i++) { var tab = _tabs[i]; var label = FitInline(labels[i], labelWidths[i]); var content = WrapBarSegment(label); segments.Add(new HeaderSegment( content, tab == _activeTab ? UiPalette.SelectionForeground : UiPalette.Text, tab == _activeTab ? UiPalette.SelectionBackground : UiPalette.HeaderTabsBackground, tab == _activeTab ? Spectre.Console.Decoration.Bold : Spectre.Console.Decoration.None)); usedWidth += content.Length; } var fillerWidth = Math.Max(availableTabsWidth - usedWidth, 0); if (fillerWidth > 0) { segments.Add(new HeaderSegment( FillBar(fillerWidth), UiPalette.Text, UiPalette.HeaderTabsBackground, Spectre.Console.Decoration.None)); } return [.. segments]; } private string GetHeaderProjectSegment() => WrapBarSegment(Localization.Current.ProjectTitle); private string GetHelpLine(Tab tab, string hint) => $"{GetTabLabel(tab)}: {hint}"; private string GetStatusBarLeftText() => $"{Localization.Label} | {Localization.Current.HelpStatusHint}"; private string GetStatusBarRightText() => $"{Localization.Current.DashboardEndpointLabel}: {_mcpEndpoint}"; private string BuildStatusBar(int width) { var left = GetStatusBarLeftText(); var right = GetStatusBarRightText(); width = Math.Max(width, 1); if (right.Length >= width) { return right[^width..]; } var leftWidth = Math.Max(width - right.Length - 1, 0); var fittedLeft = leftWidth == 0 ? string.Empty : PadRightVisible(FitInline(left, leftWidth), leftWidth); return leftWidth == 0 ? PadLeftVisible(right, width) : $"{fittedLeft} {right}"; } private static string FillBar(int width) => width <= 0 ? string.Empty : new string(Nbsp, width); private static string WrapBarSegment(string text) => $"{Nbsp}{text}{Nbsp}"; private static string PadRightVisible(string text, int width) { if (width <= 0) return string.Empty; if (text.Length >= width) return text[..width]; return text + FillBar(width - text.Length); } private static string PadLeftVisible(string text, int width) { if (width <= 0) return string.Empty; if (text.Length >= width) return text[^width..]; return FillBar(width - text.Length) + text; } private static string FitInline(string text, int width) { if (width <= 0) return string.Empty; if (text.Length <= width) return text; if (width <= 3) return text[..width]; return text[..(width - 3)] + "..."; } public void Dispose() { Registry.StateChanged -= OnRegistryChanged; LogSink.OnLog -= OnNewLog; KeyboardService.OnKeyPressed -= OnConsoleKeyPressed; Localization.OnChanged -= OnLocaleChanged; } }