@using LazyBear.MCP.Services.Logging
@using LazyBear.MCP.Services.ToolRegistry
@inject ToolRegistryService Registry
@inject InMemoryLogSink LogSink
@inject GlobalKeyboardService KeyboardService
@implements IDisposable
@foreach (var tab in _tabs)
{
var isActive = _activeTab == tab;
}
@if (_activeTab == Tab.Overview)
{
}
else if (_activeTab == Tab.Logs)
{
}
else
{
}
@code {
private enum Tab
{
Overview,
Logs,
Settings
}
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 static int GetPanelHeight() => Math.Max(Console.WindowHeight - 2, 10);
private static int GetOverviewViewportRows() => Math.Max(Console.WindowHeight - 11, 3);
private static int GetLogsViewportRows() => Math.Max(Console.WindowHeight - 16, 5);
private static int GetSettingsViewportRows() => Math.Max(Console.WindowHeight - 13, 5);
protected override void OnInitialized()
{
Registry.StateChanged += OnRegistryChanged;
LogSink.OnLog += OnNewLog;
KeyboardService.OnKeyPressed += OnConsoleKeyPressed;
}
// Конвертация 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",
_ => 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 HandleKeyDown(KeyboardEventArgs args)
{
if (string.Equals(args.Key, "Tab", StringComparison.Ordinal))
{
ChangeTab(args.ShiftKey ? -1 : 1);
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} (module is OFF, tool state is preserved)",
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 static string GetTabLabel(Tab tab) => tab switch
{
Tab.Overview => "Overview",
Tab.Logs => "Logs",
_ => "Settings"
};
public void Dispose()
{
Registry.StateChanged -= OnRegistryChanged;
LogSink.OnLog -= OnNewLog;
KeyboardService.OnKeyPressed -= OnConsoleKeyPressed;
}
}