fix: исправить навигацию клавиатуры в TUI через GlobalKeyboardService

- Добавить GlobalKeyboardService — выделенный поток с блокирующим
  Console.ReadKey, единственный источник клавишных событий для TUI
- Убрать FocusManager из App.razor: перехватывал Tab до компонентов
- Удалить @onkeydown с <Select>: RazorConsole не пробрасывает Tab/стрелки
  через этот механизм
- Использовать FocusedValue вместо Value в Select для корректной подсветки
- Обновить CLAUDE.md и AGENTS.md: архитектура TUI, RazorConsole gotchas
- Добавить docs/tui_log.md: разбор проблемы и справочник по RazorConsole

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 23:31:53 +03:00
parent 4bf267d681
commit 9b27cd7dc2
18 changed files with 1244 additions and 291 deletions

View File

@@ -1,39 +1,80 @@
@using LazyBear.MCP.Services.ToolRegistry
@inject ToolRegistryService Registry
<Rows>
<Markup Content="Module Overview" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
<Markup Content="Up/Down select a module. Enter opens its settings." Foreground="@UiPalette.TextMuted" />
<Markup Content=" " />
@foreach (var module in Registry.GetModules())
{
var (active, total) = Registry.GetToolCounts(module.ModuleName);
var isEnabled = Registry.IsModuleEnabled(module.ModuleName);
var statusColor = isEnabled ? Spectre.Console.Color.Green : Spectre.Console.Color.Red;
var statusText = isEnabled ? "ENABLED" : "DISABLED";
var activeColor = active == total
? Spectre.Console.Color.Green
: (active == 0 ? Spectre.Console.Color.Red : Spectre.Console.Color.Yellow);
<Panel Title="@module.ModuleName"
BorderColor="@(isEnabled ? Spectre.Console.Color.Green3 : Spectre.Console.Color.Grey46)"
Expand="true">
<Columns>
<Rows>
<Columns>
<Markup Content="Status: " />
<Markup Content="@statusText" Foreground="@statusColor" />
</Columns>
<Columns>
<Markup Content="Tools: " />
<Markup Content="@($"{active}/{total} active")" Foreground="@activeColor" />
</Columns>
<Markup Content="@module.Description" Foreground="@Spectre.Console.Color.Grey" />
</Rows>
</Columns>
</Panel>
@if (Rows.Count == 0)
{
<Border BorderColor="@UiPalette.Frame" BoxBorder="@Spectre.Console.BoxBorder.Rounded" Padding="@(new Spectre.Console.Padding(0, 0, 0, 0))">
<Markup Content="No modules registered." Foreground="@UiPalette.TextDim" />
</Border>
}
else
{
<Select TItem="int"
Options="@GetOptions()"
Value="@GetNormalizedIndex()"
FocusedValue="@GetNormalizedIndex()"
Formatter="@FormatRow"
Expand="true"
BorderStyle="@Spectre.Console.BoxBorder.Rounded"
SelectedIndicator="@('>')" />
}
<Markup Content=" " />
<Markup Content="[grey]Go to Settings tab to toggle modules and tools[/]" />
<Markup Content="@GetFooterText()" Foreground="@UiPalette.TextMuted" />
</Rows>
@code {
[Parameter, EditorRequired] public IReadOnlyList<OverviewRow> Rows { get; set; } = Array.Empty<OverviewRow>();
[Parameter] public int SelectedIndex { get; set; }
[Parameter] public EventCallback<int> SelectedIndexChanged { get; set; }
[Parameter] public int ViewportRows { get; set; } = 3;
private int[] GetOptions() => Enumerable.Range(0, Rows.Count).ToArray();
private int GetNormalizedIndex() => Rows.Count == 0 ? 0 : Math.Clamp(SelectedIndex, 0, Rows.Count - 1);
private string GetFooterText()
{
if (Rows.Count == 0)
{
return "No integration modules available.";
}
var selected = Rows[Math.Clamp(SelectedIndex, 0, Rows.Count - 1)];
var state = selected.IsModuleEnabled ? "ON" : "OFF";
return $"{selected.ModuleName}: {selected.Description} | Module {state} | Tools {selected.ConfiguredTools}/{selected.TotalTools}";
}
private static Spectre.Console.Color GetRowForeground(OverviewRow row) =>
row.IsModuleEnabled ? UiPalette.Text : UiPalette.TextMuted;
private string FormatRow(int index)
{
var row = Rows[index];
var status = row.IsModuleEnabled ? "[ON] " : "[OFF]";
var text = $"{row.ModuleName,-12} {status} {row.ConfiguredTools,2}/{row.TotalTools,-2} {row.Description}";
return Fit(text, Math.Max(Console.WindowWidth - 12, 32));
}
private static string Fit(string text, int width)
{
if (width <= 0)
{
return string.Empty;
}
if (text.Length <= width)
{
return text.PadRight(width);
}
if (width <= 3)
{
return text[..width];
}
return text[..(width - 3)] + "...";
}
}