Files
LazyBearWorks/AGENT.tui.md
Shahovalov MIkhail 01565b32d9 feat: добавить локализацию TUI (en/ru) с переключением клавишей L
- Добавлены TuiResources (sealed record), Locale, LocalizationService
- Все строки интерфейса вынесены из .razor-файлов в TuiResources
- App.razor: клавиша L циклически переключает локаль, заголовок показывает [EN]/[RU]
- Дочерние компоненты получают Loc как параметр (stateless)
- Создан AGENT.tui.md с правилами работы с TUI для агентов
- Обновлены AGENTS.md и CLAUDE.md со ссылками на AGENT.tui.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 23:53:59 +03:00

4.3 KiB

AGENT.tui.md

Read this file whenever you touch anything in LazyBear.MCP/TUI/.

TUI Structure

  • Entry point: TUI/Components/App.razor — owns all keyboard input and tab routing.
  • Keyboard input: TUI/GlobalKeyboardService.cs — single source; dedicated blocking thread on Console.ReadKey(intercept: true). Do not add other console readers.
  • Localization: TUI/Localization/LocalizationService singleton + TuiResources record with En/Ru static instances.
  • Components: OverviewTab, LogsTab, SettingsTab — pure display; receive state as parameters, fire no keyboard events.
  • Models: TUI/Models/OverviewRow, SettingsEntry.
  • Palette: TUI/UiPalette.cs — all colors. Do not hardcode Spectre.Console.Color values in components.

Keyboard Rules

  • All keys are handled in App.razor.HandleKeyDown(). Do not attach @onkeydown to <Select> or any other RazorConsole component — framework intercepts Tab and arrows before they reach component-level callbacks.
  • GlobalKeyboardService fires OnKeyPressed(ConsoleKeyInfo). App.razor converts via ConvertKey() and calls HandleKeyDown() inside InvokeAsync.
  • Do not use Console.KeyAvailable polling — it acquires the console mutex on every call and causes rendering lag. Always use blocking Console.ReadKey in a dedicated thread.
  • Keyboard shortcuts: Tab/Shift+Tab — switch tabs; Arrows — navigate list; Space — toggle; Enter — open/expand; L — cycle language.

RazorConsole Gotchas (0.5.0)

  • @onkeydown on <Select> is captured as AdditionalAttributes and not called for Tab/arrow keys.
  • FocusManager intercepts Tab globally — do not call FocusNextAsync() in OnAfterRenderAsync unconditionally; it shifts focus on every re-render.
  • No public global key-intercept API exists in 0.5.0.
  • Select.Value = committed selection (updated on Enter). Select.FocusedValue = highlighted item during navigation. Use FocusedValue to reflect external state immediately.
  • StateHasChanged() from a background thread must go through InvokeAsync(() => { /* mutate state */ StateHasChanged(); }).
  • Every StateHasChanged() triggers a full terminal redraw. Batch log-driven re-renders to avoid visible lag.

Localization Rules

  • All UI strings live in TUI/Localization/TuiResources.cs only. No hardcoded strings in .razor files or other .cs files.
  • To add a string: add a property to TuiResources, fill both En and Ru static instances. Build will fail if a required init is missing — by design.
  • Log level names (Info, Warn, Error) stay in English in all locales — they are technical identifiers, not UI labels.
  • Internal filter keys in App.razor ("All", "Info", "Warn", "Error") are English regardless of locale; LogsTab maps "All"Loc.FilterAll for display.
  • Language toggle: L key cycles through locales. LocalizationService.SwitchNext() fires OnChanged; App.razor re-renders via OnLocaleChanged().
  • Locale indicator shown in panel title: "LazyBear MCP [EN]" / "LazyBear MCP [RU]".
  • Do not inject LocalizationService into child tab components — App.razor passes the current TuiResources as Loc parameter. Child components are stateless regarding locale.

Component Contract

  • Child tab components (OverviewTab, LogsTab, SettingsTab) accept [Parameter] TuiResources Loc — always pass it from App.razor.
  • Child components must not subscribe to events or inject services. Keep them as pure render components.
  • SelectedIndexChanged callbacks exist for forward-compatibility; actual selection state is managed exclusively in App.razor.

DI Registration Pattern

Services that must be both injectable and run as hosted services:

services.AddSingleton<MyService>();
services.AddHostedService(sp => sp.GetRequiredService<MyService>());

AddHostedService<T>() alone creates a transient instance — unusable as injectable singleton.

Working Rules

  • Read App.razor before touching any keyboard or tab logic.
  • Match the style of the file being edited.
  • After changes, run dotnet build.
  • Do not add new hardcoded strings to .razor files — add to TuiResources instead.
  • Do not add new Console.KeyAvailable or Console.ReadKey calls outside GlobalKeyboardService.