diff --git a/LazyBear.MCP/LazyBear.MCP.csproj b/LazyBear.MCP/LazyBear.MCP.csproj
index 85acfe1..ef93b17 100644
--- a/LazyBear.MCP/LazyBear.MCP.csproj
+++ b/LazyBear.MCP/LazyBear.MCP.csproj
@@ -18,6 +18,9 @@
+
+ LazyBear.MCP
+
diff --git a/LazyBear.MCP/TUI/Components/App.razor b/LazyBear.MCP/TUI/Components/App.razor
index 44ed906..d463a88 100644
--- a/LazyBear.MCP/TUI/Components/App.razor
+++ b/LazyBear.MCP/TUI/Components/App.razor
@@ -9,37 +9,31 @@
@implements IDisposable
-
+
+ @foreach (var segment in BuildHeaderSegments())
+ {
+
+ }
+
+
+
-
-
-
-
- @foreach (var tab in _tabs)
- {
- var isActive = _activeTab == tab;
-
-
- }
-
-
-
-
@if (_activeTab == Tab.Overview)
{
}
else if (_activeTab == Tab.Logs)
@@ -62,9 +56,38 @@
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@code {
+ private const char Nbsp = '\u00A0';
+
private enum Tab
{
Overview,
@@ -72,6 +95,12 @@
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);
@@ -82,14 +111,16 @@
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(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);
+ 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()
{
@@ -115,6 +146,7 @@
ConsoleKey.PageUp => "PageUp",
ConsoleKey.PageDown => "PageDown",
ConsoleKey.Tab => "Tab",
+ ConsoleKey.Escape => "Escape",
_ => key.KeyChar == '\0' ? string.Empty : key.KeyChar.ToString()
};
@@ -145,6 +177,22 @@
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);
@@ -559,6 +607,127 @@
_ => 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;
diff --git a/LazyBear.MCP/TUI/Components/LogsTab.razor b/LazyBear.MCP/TUI/Components/LogsTab.razor
index cdd4a3b..2de5cfe 100644
--- a/LazyBear.MCP/TUI/Components/LogsTab.razor
+++ b/LazyBear.MCP/TUI/Components/LogsTab.razor
@@ -2,7 +2,6 @@
-
@@ -87,7 +86,7 @@
? selected.Message
: $"{selected.Message} | {selected.Exception}";
- return Fit(details, Math.Max(Console.WindowWidth - 12, 32));
+ return Fit(details, Math.Max(UiMetrics.ConsoleWidth - 12, 32));
}
private string FormatEntry(int index)
@@ -104,7 +103,7 @@
};
var text = $"{entry.Timestamp:HH:mm:ss} {level,-3} {entry.ShortCategory,-18} {entry.Message}";
- return Fit(text, Math.Max(Console.WindowWidth - 12, 32));
+ return Fit(text, Math.Max(UiMetrics.ConsoleWidth - 12, 32));
}
private static string Fit(string text, int width)
diff --git a/LazyBear.MCP/TUI/Components/OverviewTab.razor b/LazyBear.MCP/TUI/Components/OverviewTab.razor
index 3fd9f21..4a54647 100644
--- a/LazyBear.MCP/TUI/Components/OverviewTab.razor
+++ b/LazyBear.MCP/TUI/Components/OverviewTab.razor
@@ -1,8 +1,5 @@
-
-
-
@if (Rows.Count == 0)
@@ -32,7 +29,6 @@
[Parameter] public int SelectedIndex { get; set; }
[Parameter] public EventCallback SelectedIndexChanged { get; set; }
[Parameter] public int ViewportRows { get; set; } = 3;
- [Parameter] public string Endpoint { get; set; } = "";
[Parameter] public TuiResources Loc { get; set; } = TuiResources.En;
private int[] GetOptions() => Enumerable.Range(0, Rows.Count).ToArray();
@@ -56,7 +52,7 @@
var row = Rows[index];
var status = row.IsModuleEnabled ? $"[{Loc.StateOn}] " : $"[{Loc.StateOff}]";
var text = $"{row.ModuleName,-12} {status} {row.ConfiguredTools,2}/{row.TotalTools,-2} {row.Description}";
- return Fit(text, Math.Max(Console.WindowWidth - 12, 32));
+ return Fit(text, Math.Max(UiMetrics.ConsoleWidth - 12, 32));
}
private static string Fit(string text, int width)
diff --git a/LazyBear.MCP/TUI/Components/SettingsTab.razor b/LazyBear.MCP/TUI/Components/SettingsTab.razor
index 93769db..c4a450e 100644
--- a/LazyBear.MCP/TUI/Components/SettingsTab.razor
+++ b/LazyBear.MCP/TUI/Components/SettingsTab.razor
@@ -1,6 +1,5 @@
-
@if (Entries.Count == 0)
@@ -67,7 +66,7 @@
text = $"{indent}{checkbox} {entry.Label}{disabledSuffix}";
}
- return Fit(text, Math.Max(Console.WindowWidth - 12, 32));
+ return Fit(text, Math.Max(UiMetrics.ConsoleWidth - 12, 32));
}
private static string Fit(string text, int width)
diff --git a/LazyBear.MCP/TUI/Localization/TuiResources.cs b/LazyBear.MCP/TUI/Localization/TuiResources.cs
index 138d408..210246c 100644
--- a/LazyBear.MCP/TUI/Localization/TuiResources.cs
+++ b/LazyBear.MCP/TUI/Localization/TuiResources.cs
@@ -7,10 +7,15 @@ namespace LazyBear.MCP.TUI.Localization;
public sealed record TuiResources
{
// ── Подсказка и вкладки ──────────────────────────────────────────────────
+ public string ProjectTitle { get; init; } = "";
public string HintBar { get; init; } = "";
public string TabOverview { get; init; } = "";
public string TabLogs { get; init; } = "";
public string TabSettings { get; init; } = "";
+ public string HelpButtonLabel { get; init; } = "";
+ public string HelpStatusHint { get; init; } = "";
+ public string HelpModalTitle { get; init; } = "";
+ public string HelpCloseHint { get; init; } = "";
// ── Dashboard ────────────────────────────────────────────────────────────
public string OverviewTitle { get; init; } = "";
@@ -44,10 +49,15 @@ public sealed record TuiResources
public static readonly TuiResources En = new()
{
- HintBar = "Tab: tabs | Arrows: navigate | Space: toggle | Enter: open | L: language | Q: quit",
- TabOverview = "Dashboard",
- TabLogs = "Logs",
- TabSettings = "Settings",
+ ProjectTitle = "LazyBear MCP",
+ HintBar = "Tab/Shift+Tab: switch tabs | H: help | L: language | Q: quit",
+ TabOverview = "Dashboard",
+ TabLogs = "Logs",
+ TabSettings = "Settings",
+ HelpButtonLabel = "Help",
+ HelpStatusHint = "H: help",
+ HelpModalTitle = "Keyboard Shortcuts",
+ HelpCloseHint = "H / Esc: close",
OverviewTitle = "Dashboard",
OverviewHint = "Up/Down: select module. Enter: open settings.",
@@ -77,10 +87,15 @@ public sealed record TuiResources
public static readonly TuiResources Ru = new()
{
- HintBar = "Tab: вкладки | Стрелки: навигация | Space: вкл/выкл | Enter: открыть | L: язык | Q: выход",
- TabOverview = "Dashboard",
- TabLogs = "Логи",
- TabSettings = "Настройки",
+ ProjectTitle = "LazyBear MCP",
+ HintBar = "Tab/Shift+Tab: вкладки | H: справка | L: язык | Q: выход",
+ TabOverview = "Dashboard",
+ TabLogs = "Логи",
+ TabSettings = "Настройки",
+ HelpButtonLabel = "Справка",
+ HelpStatusHint = "H: справка",
+ HelpModalTitle = "Горячие клавиши",
+ HelpCloseHint = "H / Esc: закрыть",
OverviewTitle = "Dashboard",
OverviewHint = "Вверх/вниз: выбор модуля. Enter: открыть настройки.",
diff --git a/LazyBear.MCP/TUI/UiMetrics.cs b/LazyBear.MCP/TUI/UiMetrics.cs
new file mode 100644
index 0000000..729d89b
--- /dev/null
+++ b/LazyBear.MCP/TUI/UiMetrics.cs
@@ -0,0 +1,43 @@
+using Spectre.Console;
+
+namespace LazyBear.MCP.TUI;
+
+internal static class UiMetrics
+{
+ public static int ConsoleWidth => Math.Max(ReadConsoleSize(ReadConsoleWidth, () => AnsiConsole.Profile.Width, 80), 1);
+ public static int ConsoleHeight => Math.Max(ReadConsoleSize(ReadConsoleHeight, () => AnsiConsole.Profile.Height, 24), 1);
+
+ private static int ReadConsoleSize(Func consoleReader, Func profileReader, int fallback)
+ {
+ try
+ {
+ var consoleValue = consoleReader();
+ if (consoleValue > 0)
+ {
+ return consoleValue;
+ }
+ }
+ catch
+ {
+ // Игнорируем и пробуем fallback через профиль Spectre.
+ }
+
+ try
+ {
+ var profileValue = profileReader();
+ if (profileValue > 0)
+ {
+ return profileValue;
+ }
+ }
+ catch
+ {
+ // Игнорируем и используем значение по умолчанию.
+ }
+
+ return fallback;
+ }
+
+ private static int ReadConsoleWidth() => Console.WindowWidth;
+ private static int ReadConsoleHeight() => Console.WindowHeight;
+}
diff --git a/LazyBear.MCP/TUI/UiPalette.cs b/LazyBear.MCP/TUI/UiPalette.cs
index 6197042..5ba22cc 100644
--- a/LazyBear.MCP/TUI/UiPalette.cs
+++ b/LazyBear.MCP/TUI/UiPalette.cs
@@ -8,6 +8,8 @@ internal static class UiPalette
public static readonly Color Surface = new(12, 21, 34);
public static readonly Color SurfaceAlt = new(18, 29, 44);
public static readonly Color SurfaceMuted = new(28, 40, 56);
+ public static readonly Color HeaderBrandBackground = new(9, 31, 47);
+ public static readonly Color HeaderTabsBackground = SurfaceMuted;
public static readonly Color Accent = Color.Cyan1;
public static readonly Color AccentSoft = Color.DeepSkyBlue1;
public static readonly Color Text = Color.Grey93;
diff --git a/LazyBear.MCP/logo.svg b/LazyBear.MCP/logo.svg
new file mode 100644
index 0000000..8d289cc
--- /dev/null
+++ b/LazyBear.MCP/logo.svg
@@ -0,0 +1,77 @@
+
\ No newline at end of file