- Добавлены 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>
118 lines
4.6 KiB
Plaintext
118 lines
4.6 KiB
Plaintext
@using LazyBear.MCP.Services.Logging
|
|
|
|
<Rows>
|
|
<Markup Content="@Loc.LogsTitle" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
|
<Markup Content="@Loc.LogsHint" Foreground="@UiPalette.TextMuted" />
|
|
<Markup Content=" " />
|
|
|
|
<Columns>
|
|
@foreach (var filter in Filters)
|
|
{
|
|
var isActive = string.Equals(filter, SelectedFilter, StringComparison.Ordinal);
|
|
<Markup Content="@($" {FilterDisplay(filter)} ")"
|
|
Foreground="@(isActive ? UiPalette.SelectionForeground : UiPalette.Text)"
|
|
Background="@(isActive ? UiPalette.AccentSoft : UiPalette.SurfaceMuted)"
|
|
Decoration="@(isActive ? Spectre.Console.Decoration.Bold : Spectre.Console.Decoration.None)" />
|
|
<Markup Content=" " />
|
|
}
|
|
</Columns>
|
|
|
|
<Markup Content=" " />
|
|
|
|
@if (Entries.Count == 0)
|
|
{
|
|
<Border BorderColor="@UiPalette.Frame" BoxBorder="@Spectre.Console.BoxBorder.Rounded" Padding="@(new Spectre.Console.Padding(0, 0, 0, 0))">
|
|
<Markup Content="@Loc.LogsEmpty" Foreground="@UiPalette.TextDim" />
|
|
</Border>
|
|
}
|
|
else
|
|
{
|
|
<Select TItem="int"
|
|
Options="@GetOptions()"
|
|
Value="@GetNormalizedIndex()"
|
|
FocusedValue="@GetNormalizedIndex()"
|
|
Formatter="@FormatEntry"
|
|
Expand="true"
|
|
BorderStyle="@Spectre.Console.BoxBorder.Rounded"
|
|
SelectedIndicator="@('>')" />
|
|
}
|
|
|
|
<Markup Content=" " />
|
|
<Markup Content="@GetDetailsHeader()" Foreground="@UiPalette.TextMuted" />
|
|
<Markup Content="@GetDetailsText()" Foreground="@UiPalette.Text" />
|
|
</Rows>
|
|
|
|
@code {
|
|
// Внутренние ключи фильтров — не локализуются (используются в логике App.razor)
|
|
private static readonly string[] Filters = ["All", "Info", "Warn", "Error"];
|
|
|
|
[Parameter, EditorRequired] public IReadOnlyList<LogEntry> Entries { get; set; } = Array.Empty<LogEntry>();
|
|
[Parameter] public int SelectedIndex { get; set; }
|
|
[Parameter] public EventCallback<int> SelectedIndexChanged { get; set; }
|
|
[Parameter] public string SelectedFilter { get; set; } = "All";
|
|
[Parameter] public int ViewportRows { get; set; } = 5;
|
|
[Parameter] public bool IsStickyToBottom { get; set; }
|
|
[Parameter] public TuiResources Loc { get; set; } = TuiResources.En;
|
|
|
|
private int[] GetOptions() => Enumerable.Range(0, Entries.Count).ToArray();
|
|
|
|
private int GetNormalizedIndex() => Entries.Count == 0 ? 0 : Math.Clamp(SelectedIndex, 0, Entries.Count - 1);
|
|
|
|
// "All" локализуется; уровни логов (Info/Warn/Error) остаются на английском в любой локали
|
|
private string FilterDisplay(string filter) =>
|
|
filter == "All" ? Loc.FilterAll : filter;
|
|
|
|
private string GetDetailsHeader()
|
|
{
|
|
if (Entries.Count == 0)
|
|
{
|
|
return $"{Loc.FilterLabel}: {FilterDisplay(SelectedFilter)}";
|
|
}
|
|
|
|
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
|
|
var position = Math.Clamp(SelectedIndex, 0, Entries.Count - 1) + 1;
|
|
var sticky = IsStickyToBottom ? Loc.LogsSticky : Loc.LogsManual;
|
|
return $"{position}/{Entries.Count} | {selected.Timestamp:HH:mm:ss} | {selected.Level} | {selected.ShortCategory} | {sticky}";
|
|
}
|
|
|
|
private string GetDetailsText()
|
|
{
|
|
if (Entries.Count == 0)
|
|
{
|
|
return Loc.LogsPlaceholder;
|
|
}
|
|
|
|
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
|
|
var details = string.IsNullOrWhiteSpace(selected.Exception)
|
|
? selected.Message
|
|
: $"{selected.Message} | {selected.Exception}";
|
|
|
|
return Fit(details, Math.Max(Console.WindowWidth - 12, 32));
|
|
}
|
|
|
|
private string FormatEntry(int index)
|
|
{
|
|
var entry = Entries[index];
|
|
var level = entry.Level switch
|
|
{
|
|
LogLevel.Error => "ERR",
|
|
LogLevel.Critical => "CRT",
|
|
LogLevel.Warning => "WRN",
|
|
LogLevel.Information => "INF",
|
|
LogLevel.Debug => "DBG",
|
|
_ => "TRC"
|
|
};
|
|
|
|
var text = $"{entry.Timestamp:HH:mm:ss} {level,-3} {entry.ShortCategory,-18} {entry.Message}";
|
|
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)] + "...";
|
|
}
|
|
}
|