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

@@ -2,40 +2,87 @@ using LazyBear.MCP.Services.Confluence;
using LazyBear.MCP.Services.Jira;
using LazyBear.MCP.Services.Kubernetes;
using LazyBear.MCP.Services.Logging;
using LazyBear.MCP.Services.Mcp;
using LazyBear.MCP.Services.ToolRegistry;
using LazyBear.MCP.TUI;
using LazyBear.MCP.TUI.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RazorConsole.Core;
var builder = WebApplication.CreateBuilder(args);
// ── InMemoryLogSink: регистрируем Singleton и кастомный логгер ───────────────
// ── Общий логгер и один DI-контейнер для TUI + MCP ──────────────────────────
var logSink = new InMemoryLogSink();
builder.Services.AddSingleton(logSink);
builder.Logging.AddProvider(new InMemoryLoggerProvider(logSink));
// ── MCP-провайдеры ───────────────────────────────────────────────────────────
builder.Services.AddSingleton<K8sClientProvider>();
builder.Services.AddSingleton<JiraClientProvider>();
builder.Services.AddSingleton<ConfluenceClientProvider>();
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSingleton(logSink);
services.AddSingleton<ToolRegistryService>();
// ── ToolRegistry ─────────────────────────────────────────────────────────────
builder.Services.AddSingleton<ToolRegistryService>();
// MCP-провайдеры
services.AddSingleton<K8sClientProvider>();
services.AddSingleton<JiraClientProvider>();
services.AddSingleton<ConfluenceClientProvider>();
// ── Модули инструментов (generic: добавь новый IToolModule — он появится в TUI)
builder.Services.AddSingleton<IToolModule, JiraToolModule>();
builder.Services.AddSingleton<IToolModule, KubernetesToolModule>();
builder.Services.AddSingleton<IToolModule, ConfluenceToolModule>();
// Модули инструментов (добавь новый IToolModule — он появится в TUI)
services.AddSingleton<IToolModule, JiraToolModule>();
services.AddSingleton<IToolModule, KubernetesToolModule>();
services.AddSingleton<IToolModule, ConfluenceToolModule>();
// ── MCP-сервер ───────────────────────────────────────────────────────────────
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
// HTTP MCP endpoint запускаем в фоне, чтобы TUI оставался владельцем консоли
services.AddHostedService<McpWebHostedService>();
// ── TUI как фоновый сервис ───────────────────────────────────────────────────
builder.Services.AddHostedService<TuiHostedService>();
// Глобальный читатель клавиш — единственный источник клавишных событий для TUI
services.AddSingleton<GlobalKeyboardService>();
services.AddHostedService(sp => sp.GetRequiredService<GlobalKeyboardService>());
})
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddProvider(new InMemoryLoggerProvider(logSink));
})
.UseRazorConsole<App>(hostBuilder =>
{
hostBuilder.ConfigureServices(services =>
{
services.Configure<ConsoleAppOptions>(options =>
{
options.AutoClearConsole = true;
options.EnableTerminalResizing = true;
options.AfterRenderAsync = (_, _, _) =>
{
try
{
Console.CursorVisible = false;
}
catch
{
// Ignore terminals that do not support CursorVisible.
}
var app = builder.Build();
try
{
Console.Write("\u001b[?25l");
Console.Out.Flush();
}
catch
{
// Ignore terminals that do not support ANSI cursor control.
}
app.MapMcp();
return Task.CompletedTask;
};
});
});
})
.Build();
var urls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://localhost:5000";
app.Run(urls);
// ── Регистрируем модули один раз до старта TUI и web host ───────────────────
var registry = host.Services.GetRequiredService<ToolRegistryService>();
foreach (var module in host.Services.GetServices<IToolModule>())
{
registry.RegisterModule(module);
}
await host.RunAsync();