feat(tui): show router inference metrics beside router lines
This commit is contained in:
@@ -37,9 +37,9 @@ func PreviewFrame(kind string, w, h int) string {
|
||||
m.sessionEntered = true
|
||||
m.routerConnected = true
|
||||
m.routerMessages["04a546aa"] = []RouterEntry{
|
||||
{"user", "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."},
|
||||
{"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: "user", Content: "write a healthcheck script for the api"},
|
||||
{Role: "router", Content: "I'll create a script that pings /health and checks the status code, then writes the results to a timestamped file."},
|
||||
{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 {
|
||||
s.CurrentStage = "write_script"
|
||||
@@ -127,7 +127,7 @@ func PreviewFrame(kind string, w, h int) string {
|
||||
m.selectedID = "04a546aa"
|
||||
m.sessionEntered = true
|
||||
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
|
||||
|
||||
|
||||
@@ -50,8 +50,15 @@ const (
|
||||
|
||||
// RouterEntry is one line in a session's conversation transcript.
|
||||
type RouterEntry struct {
|
||||
Role string // user | router | tool
|
||||
Role string // user | router | tool | narration | narration_llm
|
||||
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.
|
||||
|
||||
@@ -98,7 +98,11 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
if msg.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 {
|
||||
s.LastEventAt = nowMillis()
|
||||
}
|
||||
@@ -182,7 +186,7 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
s.addEvent(msg.OccurredAt, "ToolCompleted", msg.ToolName)
|
||||
}
|
||||
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:
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -352,6 +353,9 @@ func (m Model) routerRows(w, h int) []string {
|
||||
for _, ln := range wrap(e.Content, w) {
|
||||
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":
|
||||
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
|
||||
case "narration", "stage":
|
||||
@@ -570,6 +574,14 @@ func itoa(n int) string {
|
||||
|
||||
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).
|
||||
func wrap(s string, w int) []string {
|
||||
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)",
|
||||
json: `{"type":"snapshot_complete"}`,
|
||||
|
||||
@@ -87,6 +87,10 @@ type ServerMessage struct {
|
||||
Models []string `json:"models"`
|
||||
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)
|
||||
GpuMemoryUsedMb *int64 `json:"gpuMemoryUsedMb"`
|
||||
GpuMemoryTotalMb *int64 `json:"gpuMemoryTotalMb"`
|
||||
|
||||
Reference in New Issue
Block a user