feat(server,tui): session stats pane — metrics over the WS bus + Bubble Tea overlay

Surfaces the observability metrics (observability-spec §3-tier-2) in the Go TUI:
a session-summary pane showing duration, token throughput per provider, tool
time, approval latency per tier, failure counts, and the session wall-time
accounting split. Press `S` (or palette → "session stats").

Mirrors the artifacts/config fetch pattern exactly — a WS request/response, not a
new transport. Server-side reuses the already-tested MetricsInspectionService
(one MetricsReport definition, two wires: REST `correx stats` + WS).

Server:
- ClientMessage.GetSessionStats + ServerMessage.SessionStats(@SerialName
  session.stats), reusing MetricsReport as the nested payload.
- StreamQueries.sessionStats replays via MetricsInspectionService (pure read,
  replay-neutral); routed in GlobalStreamHandler.
- ServerMessageSerializationTest golden pins the session.stats wire format.

TUI (Go/Bubble Tea):
- protocol: TypeSessionStats + StatsDto/nested structs + GetSessionStats encoder
  + golden decode test (cross-language contract).
- OverlayStats pane (statsModal), `S` key + palette entry + footer hint,
  loading/cache-by-session state, bounded breakdown rows.
- demo `stats` preview case for serverless visual verification.
This commit is contained in:
2026-06-13 10:39:52 +04:00
parent fe941408a7
commit 4f6bfa85f7
14 changed files with 387 additions and 10 deletions
+49
View File
@@ -1,5 +1,7 @@
package app
import "github.com/correx/tui-go/internal/protocol"
// PreviewFrame renders a single static frame for a named UI state at the given
// terminal size. Used by cmd/preview to screenshot the look without a live
// server. Not part of the runtime path.
@@ -138,10 +140,57 @@ func PreviewFrame(kind string, w, h int) string {
m.selectedID = "04a546aa"
m.overlay = OverlayPalette
m.paletteFilter = "to"
case "stats":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.overlay = OverlayStats
m.statsFor = "04a546aa"
m.stats = sampleStats()
}
return m.View()
}
func sampleStats() *protocol.StatsDto {
return &protocol.StatsDto{
SessionID: "04a546aa",
EventCount: 28,
SessionDurationMs: 204_000,
InferenceCount: 5,
InferenceMs: 38_400,
PromptTokens: 4120,
CompletionTokens: 1860,
TokensPerSecond: 48.4,
PerProvider: []protocol.ProviderStatsDto{
{Provider: "llama-cpp:qwen2.5-coder-14b", CompletedCount: 4, TotalLatencyMs: 33_200, PromptTokens: 3800, CompletionTokens: 1700, TokensPerSecond: 51.2},
{Provider: "llama-cpp:default", CompletedCount: 1, TotalLatencyMs: 5200, PromptTokens: 320, CompletionTokens: 160, TokensPerSecond: 30.8},
},
ToolCount: 6,
ToolMs: 4300,
PerTool: []protocol.ToolStatsDto{
{ToolName: "file_write", CompletedCount: 2, TotalDurationMs: 2600},
{ToolName: "read_file", CompletedCount: 3, TotalDurationMs: 1200},
{ToolName: "shell_exec", CompletedCount: 1, TotalDurationMs: 500},
},
ApprovalsRequested: 2,
ApprovalsResolved: 2,
ApprovalsPending: 0,
ApprovalWaitMs: 45_000,
AvgApprovalWaitMs: 22_500,
PerTier: []protocol.TierApprovalStatsDto{
{Tier: "T2", RequestedCount: 1, ResolvedCount: 1, TotalWaitMs: 15_000, AvgWaitMs: 15_000},
{Tier: "T3", RequestedCount: 1, ResolvedCount: 1, TotalWaitMs: 30_000, AvgWaitMs: 30_000},
},
Failures: protocol.FailureMetricsDto{ToolFailures: 1},
InferencePct: 18.8,
ToolPct: 2.1,
ApprovalWaitPct: 22.1,
}
}
func sampleWorkflows() []Workflow {
return []Workflow{
{ID: "healthcheck", Description: "ping endpoints and report status"},