feat(tui): Task 3.5 — decode + render router.narration as bright narration_llm lines
Add TypeRouterNarration constant, mark it event-bearing, decode golden test,
applyServer case appending RouterEntry{Role:"narration_llm"} with metrics, and
routerRows render branch showing bright ◆ prefix with FgStrong content + metrics suffix.
This commit is contained in:
@@ -92,6 +92,12 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
switch msg.Type {
|
||||
case protocol.TypeSessionAnnounced:
|
||||
m.onSessionAnnounced(msg)
|
||||
case protocol.TypeRouterNarration:
|
||||
entry := RouterEntry{Role: "narration_llm", Content: msg.Content}
|
||||
if msg.LatencyMs != nil && msg.TotalTokens != nil {
|
||||
entry.Metrics = &TurnMetrics{LatencyMs: *msg.LatencyMs, TotalTokens: *msg.TotalTokens}
|
||||
}
|
||||
m.appendRouter(msg.SessionID, entry)
|
||||
case protocol.TypeChatTurn:
|
||||
m.routerConnected = true
|
||||
role := "router"
|
||||
|
||||
@@ -358,6 +358,19 @@ func (m Model) routerRows(w, h int) []string {
|
||||
}
|
||||
case "tool":
|
||||
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
|
||||
case "narration_llm":
|
||||
diamond := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("◆")
|
||||
lines := wrap(e.Content, w-2)
|
||||
for i, ln := range lines {
|
||||
if i == 0 {
|
||||
rows = append(rows, diamond+" "+t.span(ln, t.P.FgStrong))
|
||||
} else {
|
||||
rows = append(rows, " "+t.span(ln, t.P.FgStrong))
|
||||
}
|
||||
}
|
||||
if s := metricsSuffix(e.Metrics); s != "" {
|
||||
rows = append(rows, t.span(s, t.P.Faint))
|
||||
}
|
||||
case "narration", "stage":
|
||||
rows = append(rows, t.span(e.Content, t.P.Dim))
|
||||
}
|
||||
|
||||
@@ -99,6 +99,17 @@ func TestDecodeGoldenServerFrames(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "router.narration carries content + metrics",
|
||||
json: `{"type":"router.narration","sessionId":"s1","content":"moving on","stageId":"validate","latencyMs":2000,"totalTokens":120,"sequence":10,"sessionSequence":5}`,
|
||||
wantType: TypeRouterNarration,
|
||||
eventBear: true,
|
||||
check: func(t *testing.T, m ServerMessage) {
|
||||
if m.Content != "moving on" || m.LatencyMs == nil || *m.LatencyMs != 2000 || m.TotalTokens == nil || *m.TotalTokens != 120 {
|
||||
t.Fatalf("router.narration: %+v", m)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "snapshot_complete (non-event control frame)",
|
||||
json: `{"type":"snapshot_complete"}`,
|
||||
|
||||
@@ -44,6 +44,7 @@ const (
|
||||
TypeModelChanged = "model.changed"
|
||||
TypeModelList = "model.list"
|
||||
TypeResourceStatus = "resource.status"
|
||||
TypeRouterNarration = "router.narration"
|
||||
)
|
||||
|
||||
// ServerMessage is a flat decode of every server->client variant. Field names
|
||||
@@ -186,7 +187,8 @@ func (m ServerMessage) IsEventBearing() bool {
|
||||
TypeInferenceStarted, TypeInferenceDone, TypeInferenceTimeout,
|
||||
TypeToolStarted, TypeToolCompleted, TypeToolFailed, TypeToolRejected,
|
||||
TypeToolAssessed,
|
||||
TypeApprovalRequired, TypeApprovalResolved, TypeWorkspaceBound, TypeArtifactCreated:
|
||||
TypeApprovalRequired, TypeApprovalResolved, TypeWorkspaceBound, TypeArtifactCreated,
|
||||
TypeRouterNarration:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user