feat(tui): show router inference metrics beside router lines

This commit is contained in:
2026-06-03 15:41:51 +04:00
parent c2fb17f6ee
commit 80a72d370e
6 changed files with 45 additions and 7 deletions
+4 -4
View File
@@ -37,9 +37,9 @@ func PreviewFrame(kind string, w, h int) string {
m.sessionEntered = true m.sessionEntered = true
m.routerConnected = true m.routerConnected = true
m.routerMessages["04a546aa"] = []RouterEntry{ m.routerMessages["04a546aa"] = []RouterEntry{
{"user", "write a healthcheck script for the api"}, {Role: "user", Content: "write a healthcheck script for the api"},
{"router", "I'll create a script that pings /health and checks the status code, then writes the results to a timestamped file."}, {Role: "router", Content: "I'll create a script that pings /health and checks the status code, then writes the results to a timestamped file."},
{"tool", "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,4 @@\n+#!/usr/bin/env bash\n+curl -sf http://localhost:8080/health\n+echo \"ok $(date)\"\n"}, {Role: "tool", Content: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,4 @@\n+#!/usr/bin/env bash\n+curl -sf http://localhost:8080/health\n+echo \"ok $(date)\"\n"},
} }
if s := m.session("04a546aa"); s != nil { if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script" s.CurrentStage = "write_script"
@@ -127,7 +127,7 @@ func PreviewFrame(kind string, w, h int) string {
m.selectedID = "04a546aa" m.selectedID = "04a546aa"
m.sessionEntered = true m.sessionEntered = true
m.routerMessages["04a546aa"] = []RouterEntry{ m.routerMessages["04a546aa"] = []RouterEntry{
{"tool", "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -1,2 +1,6 @@\n #!/usr/bin/env bash\n-echo hi\n+curl -sf http://localhost:8080/health\n+if [ $? -ne 0 ]; then\n+ echo \"unhealthy\" >&2\n+ exit 1\n+fi\n+echo \"ok $(date)\"\n"}, {Role: "tool", Content: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -1,2 +1,6 @@\n #!/usr/bin/env bash\n-echo hi\n+curl -sf http://localhost:8080/health\n+if [ $? -ne 0 ]; then\n+ echo \"unhealthy\" >&2\n+ exit 1\n+fi\n+echo \"ok $(date)\"\n"},
} }
m.overlay = OverlayDiff m.overlay = OverlayDiff
+8 -1
View File
@@ -50,8 +50,15 @@ const (
// RouterEntry is one line in a session's conversation transcript. // RouterEntry is one line in a session's conversation transcript.
type RouterEntry struct { type RouterEntry struct {
Role string // user | router | tool Role string // user | router | tool | narration | narration_llm
Content string Content string
Metrics *TurnMetrics
}
// TurnMetrics carries optional latency + token cost for a ROUTER chat turn.
type TurnMetrics struct {
LatencyMs int64
TotalTokens int
} }
// EventEntry is a row in the event stream. // EventEntry is a row in the event stream.
+6 -2
View File
@@ -98,7 +98,11 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if msg.Role == "USER" { if msg.Role == "USER" {
role = "user" role = "user"
} }
m.appendRouter(msg.SessionID, RouterEntry{role, msg.Content}) entry := RouterEntry{Role: role, Content: msg.Content}
if msg.LatencyMs != nil && msg.TotalTokens != nil {
entry.Metrics = &TurnMetrics{LatencyMs: *msg.LatencyMs, TotalTokens: *msg.TotalTokens}
}
m.appendRouter(msg.SessionID, entry)
if s := m.session(msg.SessionID); s != nil { if s := m.session(msg.SessionID); s != nil {
s.LastEventAt = nowMillis() s.LastEventAt = nowMillis()
} }
@@ -182,7 +186,7 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
s.addEvent(msg.OccurredAt, "ToolCompleted", msg.ToolName) s.addEvent(msg.OccurredAt, "ToolCompleted", msg.ToolName)
} }
if msg.Diff != nil && *msg.Diff != "" { if msg.Diff != nil && *msg.Diff != "" {
m.appendRouter(msg.SessionID, RouterEntry{"tool", *msg.Diff}) m.appendRouter(msg.SessionID, RouterEntry{Role: "tool", Content: *msg.Diff})
} }
case protocol.TypeToolAssessed: case protocol.TypeToolAssessed:
if s := m.session(msg.SessionID); s != nil { if s := m.session(msg.SessionID); s != nil {
+12
View File
@@ -1,6 +1,7 @@
package app package app
import ( import (
"fmt"
"os" "os"
"strings" "strings"
@@ -352,6 +353,9 @@ func (m Model) routerRows(w, h int) []string {
for _, ln := range wrap(e.Content, w) { for _, ln := range wrap(e.Content, w) {
rows = append(rows, t.span(ln, t.P.Fg)) rows = append(rows, t.span(ln, t.P.Fg))
} }
if s := metricsSuffix(e.Metrics); s != "" {
rows = append(rows, t.span(s, t.P.Faint))
}
case "tool": case "tool":
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim)) rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
case "narration", "stage": case "narration", "stage":
@@ -570,6 +574,14 @@ func itoa(n int) string {
func itoaLen(s string) string { return itoa(len(s)) } func itoaLen(s string) string { return itoa(len(s)) }
// metricsSuffix formats the faint latency+token annotation for a ROUTER turn.
func metricsSuffix(mtr *TurnMetrics) string {
if mtr == nil {
return ""
}
return fmt.Sprintf(" %.1fs · %d tokens", float64(mtr.LatencyMs)/1000.0, mtr.TotalTokens)
}
// wrap splits s into lines no wider than w (rune-based, whitespace-greedy). // wrap splits s into lines no wider than w (rune-based, whitespace-greedy).
func wrap(s string, w int) []string { func wrap(s string, w int) []string {
if w < 1 { if w < 1 {
@@ -88,6 +88,17 @@ func TestDecodeGoldenServerFrames(t *testing.T) {
} }
}, },
}, },
{
name: "chat.turn carries router metrics",
json: `{"type":"chat.turn","sessionId":"s1","turnId":"t9","role":"ROUTER","content":"hi","latencyMs":3000,"totalTokens":943,"sequence":7,"sessionSequence":2}`,
wantType: TypeChatTurn,
eventBear: true,
check: func(t *testing.T, m ServerMessage) {
if m.LatencyMs == nil || *m.LatencyMs != 3000 || m.TotalTokens == nil || *m.TotalTokens != 943 {
t.Fatalf("chat.turn metrics: %+v", m)
}
},
},
{ {
name: "snapshot_complete (non-event control frame)", name: "snapshot_complete (non-event control frame)",
json: `{"type":"snapshot_complete"}`, json: `{"type":"snapshot_complete"}`,
@@ -87,6 +87,10 @@ type ServerMessage struct {
Models []string `json:"models"` Models []string `json:"models"`
Current string `json:"current"` Current string `json:"current"`
// chat.turn router metrics (nil when not a ROUTER turn or server omits them)
LatencyMs *int64 `json:"latencyMs"`
TotalTokens *int `json:"totalTokens"`
// resource.status (nil = unavailable) // resource.status (nil = unavailable)
GpuMemoryUsedMb *int64 `json:"gpuMemoryUsedMb"` GpuMemoryUsedMb *int64 `json:"gpuMemoryUsedMb"`
GpuMemoryTotalMb *int64 `json:"gpuMemoryTotalMb"` GpuMemoryTotalMb *int64 `json:"gpuMemoryTotalMb"`